use crate::frontends::native::egui_renderer::{EguiFrameInput, NativeEguiRenderer};
use crate::frontends::native::egui_texture::NativeTextureName;
use crate::frontends::native::input::{EguiInputState, InputEvent};
use crate::frontends::native::shader_manager::ShaderManager;
use crate::gb::debugging::GbDebuggerViewState;
use crate::gb::debugging::debugger_ui as gb_debugger_ui;
use crate::nes::console::Nes;
use crate::nes::debugging::DebuggerViewState;
use crate::nes::debugging::ppu_viewer::{
PpuViewerSnapshot, render_nametables_rgba, render_pattern_tables_rgba,
};
use crate::nes::debugging::ui::{
self as debugger_ui, BreakpointAddUiState, HexdumpUiState, WatchlistUiState,
};
use crate::platform::app_context::SharedAppContext;
use crate::platform::debugging::breakpoints::BreakpointList;
use crate::platform::debugging::log_info;
use crate::platform::emulator::Console;
use std::ffi::c_void;
use std::rc::Rc;
use std::time::Instant;
const PPU_VIEWER_NT_TEXTURE_WIDTH: i32 = 512;
const PPU_VIEWER_NT_TEXTURE_HEIGHT: i32 = 480;
const PPU_VIEWER_TILES_TEXTURE_WIDTH: i32 = 256;
const PPU_VIEWER_TILES_TEXTURE_HEIGHT: i32 = 128;
const PPU_VIEWER_WINDOW_INITIAL_WIDTH: f32 = 560.0;
const PPU_VIEWER_WINDOW_INITIAL_HEIGHT: f32 = 854.0;
const GB_PPU_VIEWER_TILES_TEXTURE_WIDTH_DMG: i32 = 256;
const GB_PPU_VIEWER_TILES_TEXTURE_HEIGHT_DMG: i32 = 128;
const GB_PPU_VIEWER_TILES_TEXTURE_WIDTH_CGB: i32 = 512;
const GB_PPU_VIEWER_TILES_TEXTURE_HEIGHT_CGB: i32 = 128;
const GB_PPU_VIEWER_BG_MAPS_TEXTURE_WIDTH: i32 = 512;
const GB_PPU_VIEWER_BG_MAPS_TEXTURE_HEIGHT: i32 = 256;
const GB_PPU_VIEWER_WINDOW_INITIAL_WIDTH: f32 = 560.0;
const GB_PPU_VIEWER_WINDOW_INITIAL_HEIGHT: f32 = 600.0;
pub trait RenderTarget {
fn window_size(&self) -> (u32, u32);
fn drawable_size(&self) -> (u32, u32);
fn swap_buffers(&self);
fn make_current(&self) -> Result<(), String>;
fn set_fullscreen(&mut self, enabled: bool) -> Result<(), String>;
fn set_mouse_grab(&mut self, enabled: bool) -> Result<(), String>;
fn notify_resize(&mut self, _w: u32, _h: u32) {}
}
pub type ProcAddressLoader = Rc<dyn Fn(&str) -> *const c_void>;
pub struct GlBackend {
render_target: Box<dyn RenderTarget>,
glow_context: std::sync::Arc<glow::Context>,
egui_renderer: NativeEguiRenderer,
egui_input: EguiInputState,
nes_texture: gl::types::GLuint,
nes_egui_texture_id: egui::TextureId,
shader_output_egui_texture: Option<RegisteredEguiTexture>,
ppu_viewer_nt_texture: gl::types::GLuint,
ppu_viewer_nt_texture_id: egui::TextureId,
ppu_viewer_tiles_texture: gl::types::GLuint,
ppu_viewer_tiles_texture_id: egui::TextureId,
overlay_text_color: OverlayTextColor,
app_context: SharedAppContext,
framebuffer: Vec<u8>,
last_frame: Instant,
debugger_view_state: DebuggerViewState,
debugger_alpha: f32,
breakpoints: BreakpointList,
bp_add_state: BreakpointAddUiState,
hexdump_ui_state: HexdumpUiState,
watchlist_ui_state: WatchlistUiState,
gb_debugger_view_state: GbDebuggerViewState,
gb_debugger_alpha: f32,
gb_breakpoints: BreakpointList,
gb_bp_add_state: gb_debugger_ui::BreakpointAddUiState,
gb_hexdump_ui_state: gb_debugger_ui::HexdumpUiState,
gb_watchlist_ui_state: gb_debugger_ui::WatchlistUiState,
last_gb_action: gb_debugger_ui::GbDebuggerUiAction,
gb_ppu_viewer_tiles_texture: gl::types::GLuint,
gb_ppu_viewer_tiles_texture_id: egui::TextureId,
gb_ppu_viewer_bg_maps_texture: gl::types::GLuint,
gb_ppu_viewer_bg_maps_texture_id: egui::TextureId,
shader_manager: ShaderManager,
tex_w: u32,
tex_h: u32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Crosshair {
pub x: f32,
pub y: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OverlayTextColor {
White,
Black,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct RegisteredEguiTexture {
gl_id: gl::types::GLuint,
egui_id: egui::TextureId,
}
impl RegisteredEguiTexture {
fn matches_gl_id(self, gl_id: gl::types::GLuint) -> bool {
self.gl_id == gl_id
}
}
fn crosshair_rgba() -> [f32; 4] {
[1.0, 0.2, 0.2, 1.0]
}
struct CrosshairDrawContext {
x0: f32,
y0: f32,
draw_w: f32,
draw_h: f32,
cropped_w: u32,
cropped_h: u32,
h_overscan: u32,
v_overscan: u32,
}
fn project_crosshair_to_cropped_indices(
crosshair: Crosshair,
draw_ctx: &CrosshairDrawContext,
) -> (f32, f32) {
let nes_x = crosshair.x.floor();
let nes_y = crosshair.y.floor();
let ix = (nes_x - draw_ctx.h_overscan as f32)
.clamp(0.0, (draw_ctx.cropped_w.saturating_sub(1)) as f32);
let iy = (nes_y - draw_ctx.v_overscan as f32)
.clamp(0.0, (draw_ctx.cropped_h.saturating_sub(1)) as f32);
(ix, iy)
}
fn fps_counter_text(fps: usize) -> String {
format!("{fps} FPS")
}
impl OverlayTextColor {
fn toggle(self) -> Self {
match self {
OverlayTextColor::White => OverlayTextColor::Black,
OverlayTextColor::Black => OverlayTextColor::White,
}
}
fn rgba(self) -> [f32; 4] {
match self {
OverlayTextColor::White => [1.0, 1.0, 1.0, 1.0],
OverlayTextColor::Black => [0.0, 0.0, 0.0, 1.0],
}
}
}
fn draw_egui_frame_background(
ui: &mut egui::Ui,
texture_id: egui::TextureId,
x0: f32,
y0: f32,
draw_w: f32,
draw_h: f32,
) {
let rect = egui::Rect::from_min_max(egui::pos2(x0, y0), egui::pos2(x0 + draw_w, y0 + draw_h));
ui.painter().image(
texture_id,
rect,
egui::Rect::from_min_max(egui::Pos2::ZERO, egui::pos2(1.0, 1.0)),
egui::Color32::WHITE,
);
}
fn draw_egui_overlay_text(
ui: &mut egui::Ui,
text: &str,
text_color: OverlayTextColor,
blink_red: bool,
x0: f32,
y0: f32,
) {
let painter = ui.painter();
let overlay_style = text_color;
let egui_text_color = egui_color_from_rgba(overlay_text_rgba(overlay_style, blink_red));
let background_color = egui_color_from_rgba(overlay_background_color_for(overlay_style));
let galley = painter.layout_no_wrap(text.to_owned(), overlay_egui_font_id(), egui_text_color);
let text_size = [galley.size().x, galley.size().y];
let layout = crate::frontends::native::ui_geometry::top_left_text_panel(
[x0, y0],
text_size,
[8.0, 8.0],
[6.0, 4.0],
);
painter.rect_filled(
egui::Rect::from_min_max(
egui::pos2(layout.rect_min[0], layout.rect_min[1]),
egui::pos2(layout.rect_max[0], layout.rect_max[1]),
),
0.0,
background_color,
);
painter.galley(
egui::pos2(layout.text_pos[0], layout.text_pos[1]),
galley,
egui_text_color,
);
}
fn draw_egui_fps_counter(ui: &mut egui::Ui, fps: usize, x0: f32, y0: f32, draw_w: f32) {
let painter = ui.painter();
let text = fps_counter_text(fps);
let text_color = egui::Color32::YELLOW;
let galley = painter.layout_no_wrap(text, overlay_egui_font_id(), text_color);
let text_size = [galley.size().x, galley.size().y];
let layout = crate::frontends::native::ui_geometry::top_right_text_panel(
[x0, y0],
draw_w,
text_size,
[8.0, 8.0],
[6.0, 4.0],
);
painter.rect_filled(
egui::Rect::from_min_max(
egui::pos2(layout.rect_min[0], layout.rect_min[1]),
egui::pos2(layout.rect_max[0], layout.rect_max[1]),
),
3.0,
egui_color_from_rgba([0.0, 0.0, 0.0, 0.6]),
);
painter.galley(
egui::pos2(layout.text_pos[0], layout.text_pos[1]),
galley,
text_color,
);
}
fn draw_egui_toasts(
ui: &mut egui::Ui,
visible_toasts: &[String],
x0: f32,
y0: f32,
draw_w: f32,
draw_h: f32,
) {
let painter = ui.painter();
let text_color = egui_color_from_rgba(toast_text_rgba());
let background_color = egui_color_from_rgba(toast_background_rgba());
let padding = [8.0, 6.0];
let spacing = 8.0;
let bottom_margin = 12.0;
for (stack_index, toast_text) in visible_toasts.iter().rev().enumerate() {
let galley =
painter.layout_no_wrap(toast_text.to_owned(), overlay_egui_font_id(), text_color);
let text_size = [galley.size().x, galley.size().y];
let layout = crate::frontends::native::ui_geometry::bottom_center_text_panel(
[x0, y0],
[draw_w, draw_h],
text_size,
stack_index,
bottom_margin,
spacing,
padding,
);
painter.rect_filled(
egui::Rect::from_min_max(
egui::pos2(layout.rect_min[0], layout.rect_min[1]),
egui::pos2(layout.rect_max[0], layout.rect_max[1]),
),
0.0,
background_color,
);
painter.galley(
egui::pos2(layout.text_pos[0], layout.text_pos[1]),
galley,
text_color,
);
}
}
fn draw_egui_crosshair(ui: &mut egui::Ui, crosshair: Crosshair, draw_ctx: &CrosshairDrawContext) {
let painter = ui.painter();
let color = egui_color_from_rgba(crosshair_rgba());
let (ix, iy) = project_crosshair_to_cropped_indices(crosshair, draw_ctx);
let rects = crate::frontends::native::ui_geometry::crosshair_marker_rects(
[draw_ctx.x0, draw_ctx.y0],
[draw_ctx.draw_w, draw_ctx.draw_h],
[draw_ctx.cropped_w, draw_ctx.cropped_h],
[ix, iy],
);
for rect in rects {
painter.rect_filled(
egui::Rect::from_min_max(
egui::pos2(rect.rect_min[0], rect.rect_min[1]),
egui::pos2(rect.rect_max[0], rect.rect_max[1]),
),
0.0,
color,
);
}
}
fn overlay_egui_font_id() -> egui::FontId {
egui::FontId::proportional(26.0)
}
fn egui_color_from_rgba(rgba: [f32; 4]) -> egui::Color32 {
egui::Color32::from_rgba_unmultiplied(
(rgba[0].clamp(0.0, 1.0) * 255.0).round() as u8,
(rgba[1].clamp(0.0, 1.0) * 255.0).round() as u8,
(rgba[2].clamp(0.0, 1.0) * 255.0).round() as u8,
(rgba[3].clamp(0.0, 1.0) * 255.0).round() as u8,
)
}
fn overlay_text_rgba(text_color: OverlayTextColor, blink_red: bool) -> [f32; 4] {
if blink_red {
[1.0, 0.0, 0.0, 1.0]
} else {
text_color.rgba()
}
}
fn overlay_background_color_for(text_color: OverlayTextColor) -> [f32; 4] {
match text_color {
OverlayTextColor::White => [0.0, 0.0, 0.0, 0.5],
OverlayTextColor::Black => [1.0, 1.0, 1.0, 0.5],
}
}
fn toast_text_rgba() -> [f32; 4] {
[1.0, 1.0, 1.0, 1.0]
}
fn toast_background_rgba() -> [f32; 4] {
[0.35, 0.35, 0.35, 0.7]
}
fn egui_frame_input_for_window(
window_size: (u32, u32),
drawable_size: (u32, u32),
predicted_dt: f32,
) -> EguiFrameInput {
let (win_w, win_h) = window_size;
let (drawable_w, drawable_h) = drawable_size;
EguiFrameInput::new(
[win_w as f32, win_h as f32],
[drawable_w, drawable_h],
egui_pixels_per_point(window_size, drawable_size),
predicted_dt,
)
}
fn egui_pixels_per_point(window_size: (u32, u32), drawable_size: (u32, u32)) -> f32 {
let (win_w, win_h) = window_size;
let (drawable_w, drawable_h) = drawable_size;
if win_w > 0 {
drawable_w as f32 / win_w as f32
} else if win_h > 0 {
drawable_h as f32 / win_h as f32
} else {
1.0
}
}
impl GlBackend {
fn target_aspect(&self, pixel_aspect: f32) -> f32 {
let w = self.tex_w as f32;
let h = self.tex_h as f32;
if h == 0.0 {
return 1.0;
}
(w / h) * pixel_aspect
}
pub fn window_size(&self) -> (u32, u32) {
self.render_target.window_size()
}
pub fn set_fullscreen(&mut self, enabled: bool) -> Result<(), String> {
self.render_target.set_fullscreen(enabled)
}
pub fn set_mouse_grab(&mut self, enabled: bool) -> Result<(), String> {
self.render_target.set_mouse_grab(enabled)
}
pub fn notify_resize(&mut self, w: u32, h: u32) {
self.render_target.notify_resize(w, h);
}
fn register_shader_output_texture(
&mut self,
gl_id: gl::types::GLuint,
) -> Result<egui::TextureId, String> {
if let Some(registered) = self.shader_output_egui_texture
&& registered.matches_gl_id(gl_id)
{
return Ok(registered.egui_id);
}
let output_texture = self
.shader_manager
.take_output_texture_ownership(gl_id)
.ok_or_else(|| "ShaderManager did not own the shader output texture".to_owned())?;
let texture_name = NativeTextureName::from_gl_id(output_texture.gl_id())
.ok_or_else(|| "Shader output texture ID was zero".to_owned())?;
if let Some(registered) = self.shader_output_egui_texture.take() {
self.egui_renderer.free_texture(registered.egui_id);
}
let egui_id = self.egui_renderer.register_native_texture(texture_name);
self.shader_output_egui_texture = Some(RegisteredEguiTexture {
gl_id: output_texture.gl_id(),
egui_id,
});
Ok(egui_id)
}
pub fn new(
render_target: Box<dyn RenderTarget>,
proc_address: ProcAddressLoader,
shader_path: Option<&str>,
app_context: SharedAppContext,
allowed_shaders: &[&str],
) -> Result<Self, String> {
unsafe {
gl::Disable(gl::DEPTH_TEST);
gl::Disable(gl::CULL_FACE);
let (drawable_w, drawable_h) = render_target.drawable_size();
gl::Viewport(0, 0, drawable_w as i32, drawable_h as i32);
gl::ClearColor(0.0, 0.0, 0.0, 1.0);
}
let glow_context = unsafe {
let proc_address = proc_address.clone();
std::sync::Arc::new(glow::Context::from_loader_function(|s| {
(proc_address)(s) as *const _
}))
};
let egui_glow_context = unsafe {
let proc_address = proc_address.clone();
std::sync::Arc::new(egui_glow::glow::Context::from_loader_function(|s| {
(proc_address)(s) as *const _
}))
};
let mut egui_renderer = NativeEguiRenderer::new(egui_glow_context)?;
let (nes_texture, nes_egui_texture_id) = unsafe {
let mut tex: gl::types::GLuint = 0;
gl::GenTextures(1, &mut tex);
let texture_name = NativeTextureName::from_gl_id(tex)
.ok_or_else(|| "Failed to create emulator frame texture".to_owned())?;
gl::BindTexture(gl::TEXTURE_2D, tex);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::NEAREST as i32);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::NEAREST as i32);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32);
gl::PixelStorei(gl::UNPACK_ALIGNMENT, 1);
gl::TexImage2D(
gl::TEXTURE_2D,
0,
gl::RGB8 as i32,
1,
1,
0,
gl::RGB,
gl::UNSIGNED_BYTE,
std::ptr::null(),
);
let egui_id = egui_renderer.register_native_texture(texture_name);
(tex, egui_id)
};
let (ppu_viewer_nt_texture, ppu_viewer_nt_texture_id) = unsafe {
create_egui_rgba_texture(
&mut egui_renderer,
PPU_VIEWER_NT_TEXTURE_WIDTH,
PPU_VIEWER_NT_TEXTURE_HEIGHT,
)?
};
let (ppu_viewer_tiles_texture, ppu_viewer_tiles_texture_id) = unsafe {
create_egui_rgba_texture(
&mut egui_renderer,
PPU_VIEWER_TILES_TEXTURE_WIDTH,
PPU_VIEWER_TILES_TEXTURE_HEIGHT,
)?
};
let (gb_ppu_viewer_tiles_texture, gb_ppu_viewer_tiles_texture_id) = unsafe {
create_egui_rgba_texture(
&mut egui_renderer,
GB_PPU_VIEWER_TILES_TEXTURE_WIDTH_CGB,
GB_PPU_VIEWER_TILES_TEXTURE_HEIGHT_CGB,
)?
};
let (gb_ppu_viewer_bg_maps_texture, gb_ppu_viewer_bg_maps_texture_id) = unsafe {
create_egui_rgba_texture(
&mut egui_renderer,
GB_PPU_VIEWER_BG_MAPS_TEXTURE_WIDTH,
GB_PPU_VIEWER_BG_MAPS_TEXTURE_HEIGHT,
)?
};
let mut shader_manager = ShaderManager::new(allowed_shaders);
if let Some(path) = shader_path
&& let Err(e) =
shader_manager.load_preset(std::path::Path::new(path), glow_context.clone())
{
log_info(format!(
"Warning: Failed to load shader preset '{}': {}",
path, e
));
}
Ok(Self {
render_target,
glow_context,
egui_renderer,
egui_input: EguiInputState::default(),
nes_texture,
nes_egui_texture_id,
shader_output_egui_texture: None,
ppu_viewer_nt_texture,
ppu_viewer_nt_texture_id,
ppu_viewer_tiles_texture,
ppu_viewer_tiles_texture_id,
overlay_text_color: OverlayTextColor::White,
app_context,
framebuffer: Vec::new(),
last_frame: Instant::now(),
debugger_view_state: DebuggerViewState::default(),
debugger_alpha: 1.0,
breakpoints: BreakpointList::default(),
bp_add_state: BreakpointAddUiState::default(),
hexdump_ui_state: HexdumpUiState::default(),
watchlist_ui_state: WatchlistUiState::default(),
gb_debugger_view_state: GbDebuggerViewState::default(),
gb_debugger_alpha: 1.0,
gb_breakpoints: BreakpointList::default(),
gb_bp_add_state: gb_debugger_ui::BreakpointAddUiState::default(),
gb_hexdump_ui_state: gb_debugger_ui::HexdumpUiState::default(),
gb_watchlist_ui_state: gb_debugger_ui::WatchlistUiState::default(),
last_gb_action: gb_debugger_ui::GbDebuggerUiAction::default(),
gb_ppu_viewer_tiles_texture,
gb_ppu_viewer_tiles_texture_id,
gb_ppu_viewer_bg_maps_texture,
gb_ppu_viewer_bg_maps_texture_id,
shader_manager,
tex_w: 1,
tex_h: 1,
})
}
pub fn handle_input(&mut self, event: &InputEvent) {
if let InputEvent::Key {
key: crate::frontends::native::input::UiKey::F1,
down: true,
} = event
{
self.overlay_text_color = self.overlay_text_color.toggle();
}
self.egui_input.apply_input(event);
}
pub fn set_debugger_alpha(&mut self, alpha: f32) {
self.debugger_alpha = alpha.clamp(0.1, 1.0);
}
pub fn update_breakpoints(&mut self, breakpoints: &BreakpointList) {
self.breakpoints = breakpoints.clone();
}
#[allow(dead_code)] pub fn set_gb_debugger_alpha(&mut self, alpha: f32) {
self.gb_debugger_alpha = alpha.clamp(0.1, 1.0);
}
pub fn update_gb_breakpoints(&mut self, breakpoints: &BreakpointList) {
self.gb_breakpoints = breakpoints.clone();
}
pub fn take_gb_debugger_action(&mut self) -> gb_debugger_ui::GbDebuggerUiAction {
std::mem::take(&mut self.last_gb_action)
}
pub fn watch_addresses(&self) -> Vec<u16> {
self.debugger_view_state.watch_addresses()
}
pub fn set_watch_addresses(&mut self, addresses: Vec<u16>) {
self.debugger_view_state.clear_watch_addresses();
for addr in addresses {
self.debugger_view_state.add_watch_address(addr);
}
}
pub fn render(
&mut self,
console: &Console,
show_debugger: bool,
overlay_text: Option<&str>,
overlay_blink_red: bool,
crosshair: Option<Crosshair>,
fps: Option<usize>,
) -> debugger_ui::DebuggerUiAction {
let mut action = debugger_ui::DebuggerUiAction::default();
let now = Instant::now();
let dt = now.saturating_duration_since(self.last_frame);
self.last_frame = now;
let predicted_dt = dt.as_secs_f32().max(1.0 / 1000.0);
let (win_w, win_h) = self.render_target.window_size();
let (drawable_w, drawable_h) = self.render_target.drawable_size();
let egui_frame_input =
egui_frame_input_for_window((win_w, win_h), (drawable_w, drawable_h), predicted_dt);
unsafe {
gl::Viewport(0, 0, drawable_w as i32, drawable_h as i32);
}
let (h_overscan, v_overscan) = console.overscan();
let (frame_w, frame_h) = console.cropped_dims(h_overscan, v_overscan);
{
let cropped = console.cropped_screen_snapshot(h_overscan, v_overscan);
let required = (frame_w * frame_h * 3) as usize;
if self.framebuffer.len() != required {
self.framebuffer.resize(required, 0);
}
self.framebuffer.copy_from_slice(&cropped);
}
let tex_w = frame_w as i32;
let tex_h = frame_h as i32;
unsafe {
gl::BindTexture(gl::TEXTURE_2D, self.nes_texture);
if self.tex_w != frame_w || self.tex_h != frame_h {
gl::TexImage2D(
gl::TEXTURE_2D,
0,
gl::RGB8 as i32,
tex_w,
tex_h,
0,
gl::RGB,
gl::UNSIGNED_BYTE,
std::ptr::null(),
);
self.tex_w = frame_w;
self.tex_h = frame_h;
}
gl::PixelStorei(gl::UNPACK_ALIGNMENT, 1);
gl::TexSubImage2D(
gl::TEXTURE_2D,
0,
0,
0,
tex_w,
tex_h,
gl::RGB,
gl::UNSIGNED_BYTE,
self.framebuffer.as_ptr() as *const _,
);
gl::GenerateMipmap(gl::TEXTURE_2D);
gl::Clear(gl::COLOR_BUFFER_BIT);
}
let target_aspect = self.target_aspect(console.pixel_aspect());
let mut shader_output_texture_id: Option<egui::TextureId> = None;
if self.shader_manager.has_shader() {
let drawable_w_f = drawable_w as f32;
let drawable_h_f = drawable_h as f32;
let (shader_out_w_f, shader_out_h_f) =
crate::frontends::native::ui_geometry::letterbox_size(
drawable_w_f,
drawable_h_f,
target_aspect,
);
let shader_out_w = (shader_out_w_f as u32).max(1);
let shader_out_h = (shader_out_h_f as u32).max(1);
if let Err(e) = self.shader_manager.apply_shader(
self.nes_texture,
tex_w as u32,
tex_h as u32,
shader_out_w,
shader_out_h,
) {
log_info(format!("Shader application error: {}", e));
} else if let Some(tex) = self.shader_manager.output_texture() {
match self.register_shader_output_texture(tex) {
Ok(texture_id) => shader_output_texture_id = Some(texture_id),
Err(e) => log_info(format!("Shader texture registration error: {}", e)),
}
}
}
let visible_toasts = self.app_context.borrow_mut().visible_toasts(now);
let nes_ppu_viewer_scroll = if show_debugger
&& let Console::Nes(nes) = console
&& self.debugger_view_state.is_ppu_viewer_visible()
{
Some(update_ppu_viewer_textures(
nes,
self.ppu_viewer_nt_texture,
self.ppu_viewer_tiles_texture,
))
} else {
None
};
let gb_ppu_viewer_snapshot = if show_debugger
&& let Console::GameBoy(gb) = console
&& self.gb_debugger_view_state.is_ppu_viewer_visible()
{
let ppu_snap = gb.create_ppu_viewer_snapshot();
update_gb_ppu_viewer_textures_from_snapshot(
&ppu_snap,
self.gb_ppu_viewer_tiles_texture,
self.gb_ppu_viewer_bg_maps_texture,
);
Some(ppu_snap)
} else {
None
};
let nes_debugger_snapshot = if show_debugger && let Console::Nes(nes) = console {
Some(self.debugger_view_state.snapshot(nes))
} else {
None
};
let gb_debugger_snapshot = if show_debugger && let Console::GameBoy(gb) = console {
Some(gb.create_debugger_snapshot(&mut self.gb_debugger_view_state))
} else {
None
};
let cropped_w = self.tex_w;
let cropped_h = self.tex_h;
let win_w = win_w as f32;
let win_h = win_h as f32;
let frame_rect = crate::frontends::native::ui_geometry::letterbox_rect(
[0.0, 0.0],
[win_w, win_h],
target_aspect,
);
let x0 = frame_rect.rect_min[0];
let y0 = frame_rect.rect_min[1];
let [draw_w, draw_h] = frame_rect.size();
let crosshair_draw_ctx = CrosshairDrawContext {
x0,
y0,
draw_w,
draw_h,
cropped_w,
cropped_h,
h_overscan,
v_overscan,
};
let egui_texture_id = shader_output_texture_id.unwrap_or(self.nes_egui_texture_id);
let ppu_viewer_nt_texture_id = self.ppu_viewer_nt_texture_id;
let ppu_viewer_tiles_texture_id = self.ppu_viewer_tiles_texture_id;
let gb_ppu_viewer_tiles_texture_id = self.gb_ppu_viewer_tiles_texture_id;
let gb_ppu_viewer_bg_maps_texture_id = self.gb_ppu_viewer_bg_maps_texture_id;
let debugger_alpha = self.debugger_alpha;
let breakpoints = &self.breakpoints;
let bp_add_state = &mut self.bp_add_state;
let hexdump_ui_state = &mut self.hexdump_ui_state;
let watchlist_ui_state = &mut self.watchlist_ui_state;
let mut nes_debugger_action = None;
let gb_debugger_alpha = self.gb_debugger_alpha;
let gb_breakpoints = &self.gb_breakpoints;
let gb_bp_add_state = &mut self.gb_bp_add_state;
let gb_hexdump_ui_state = &mut self.gb_hexdump_ui_state;
let gb_watchlist_ui_state = &mut self.gb_watchlist_ui_state;
let mut gb_debugger_action = None;
let egui_paint_data =
self.egui_renderer
.run(egui_frame_input, &mut self.egui_input, |ui| {
draw_egui_frame_background(ui, egui_texture_id, x0, y0, draw_w, draw_h);
if let Some(text) = overlay_text {
draw_egui_overlay_text(
ui,
text,
self.overlay_text_color,
overlay_blink_red,
x0,
y0,
);
}
if let Some(crosshair) = crosshair {
draw_egui_crosshair(ui, crosshair, &crosshair_draw_ctx);
}
if let Some(fps_value) = fps {
draw_egui_fps_counter(ui, fps_value, x0, y0, draw_w);
}
if !visible_toasts.is_empty() {
draw_egui_toasts(ui, &visible_toasts, x0, y0, draw_w, draw_h);
}
if let Some(scroll) = nes_ppu_viewer_scroll {
draw_ppu_viewer_window(
ui,
ppu_viewer_nt_texture_id,
ppu_viewer_tiles_texture_id,
scroll,
);
}
if let Some(ppu_snap) = gb_ppu_viewer_snapshot.as_ref() {
draw_gb_ppu_viewer_window(
ui,
gb_ppu_viewer_tiles_texture_id,
gb_ppu_viewer_bg_maps_texture_id,
ppu_snap,
);
}
if let Some(snapshot) = nes_debugger_snapshot.as_ref() {
nes_debugger_action = Some(debugger_ui::render(
ui,
snapshot,
debugger_alpha,
breakpoints,
bp_add_state,
hexdump_ui_state,
watchlist_ui_state,
));
}
if let Some(snapshot) = gb_debugger_snapshot.as_ref() {
gb_debugger_action = Some(gb_debugger_ui::render(
ui,
snapshot,
gb_debugger_alpha,
gb_breakpoints,
gb_bp_add_state,
gb_hexdump_ui_state,
gb_watchlist_ui_state,
));
}
});
self.egui_renderer.paint(egui_paint_data);
if let (Some(action_from_ui), Some(snapshot)) =
(nes_debugger_action, nes_debugger_snapshot.as_ref())
{
action = action_from_ui;
if action.toggle_ppu_viewer {
self.debugger_view_state.toggle_ppu_viewer();
}
if let Some(base) = action.set_prg_hexdump_base {
self.debugger_view_state.set_prg_hexdump_base(base);
}
if let Some(delta) = action.nudge_prg_hexdump_base_by_bytes {
self.debugger_view_state
.nudge_prg_hexdump_base_by_bytes_from(snapshot.prg_hexdump_base, delta);
}
if let Some(address) = action.add_watch_address {
self.debugger_view_state.add_watch_address(address);
}
if let Some(index) = action.remove_watch_address {
self.debugger_view_state.remove_watch_address(index);
}
if let Some(update) = action.update_watch_address {
self.debugger_view_state
.update_watch_address(update.index, update.address);
}
if action.increase_opacity {
self.debugger_alpha = (self.debugger_alpha + 0.1).min(1.0);
}
if action.decrease_opacity {
self.debugger_alpha = (self.debugger_alpha - 0.1).max(0.1);
}
}
if let (Some(gb_action), Some(snapshot)) =
(gb_debugger_action, gb_debugger_snapshot.as_ref())
{
if gb_action.toggle_ppu_viewer {
self.gb_debugger_view_state.toggle_ppu_viewer();
}
if let Some(base) = gb_action.set_wram_hexdump_base {
self.gb_debugger_view_state.set_wram_hexdump_base(base);
}
if let Some(delta) = gb_action.nudge_wram_hexdump_base_by_bytes {
self.gb_debugger_view_state
.nudge_wram_hexdump_base_by_bytes_from(snapshot.wram_hexdump_base, delta);
}
if let Some(base) = gb_action.set_vram_hexdump_base {
self.gb_debugger_view_state.set_vram_hexdump_base(base);
}
if let Some(delta) = gb_action.nudge_vram_hexdump_base_by_bytes {
self.gb_debugger_view_state
.nudge_vram_hexdump_base_by_bytes_from(snapshot.vram_hexdump_base, delta);
}
if let Some(address) = gb_action.add_watch_address {
self.gb_debugger_view_state.add_watch_address(address);
}
if let Some(index) = gb_action.remove_watch_address {
self.gb_debugger_view_state.remove_watch_address(index);
}
if let Some(update) = gb_action.update_watch_address {
self.gb_debugger_view_state
.update_watch_address(update.index, update.address);
}
if gb_action.increase_opacity {
self.gb_debugger_alpha = (self.gb_debugger_alpha + 0.1).min(1.0);
}
if gb_action.decrease_opacity {
self.gb_debugger_alpha = (self.gb_debugger_alpha - 0.1).max(0.1);
}
self.last_gb_action = gb_action;
}
self.render_target.swap_buffers();
action
}
pub fn cycle_shader(&mut self) -> Option<String> {
if let Err(e) = self.shader_manager.cycle_shader(self.glow_context.clone()) {
log_info(format!("Error cycling shader: {}", e));
}
let path = self.shader_manager.current_preset_name();
let short_name = path.and_then(|p| {
crate::platform::shaders::SHADER_PRESETS
.iter()
.find(|(_, preset_path)| *preset_path == p)
.map(|(name, _)| (*name).to_owned())
});
log_info(format!(
"Switched to filter: {}",
short_name.as_deref().unwrap_or("off")
));
short_name
}
}
pub fn shader_toast_message(preset_name: Option<&str>) -> String {
match preset_name {
Some(name) => format!("Visual Filter: {}", name.to_uppercase()),
None => "Visual Filter: off".to_owned(),
}
}
#[cfg(test)]
mod tests_letterbox {
use crate::frontends::native::ui_geometry::letterbox_size;
const NTSC_ASPECT: f32 = 8.0 / 7.0 * 16.0 / 15.0;
#[test]
fn test_letterbox_size_wide_container() {
let (w, h) = letterbox_size(1920.0, 1080.0, NTSC_ASPECT);
assert!((w - 1316.5714).abs() < 0.01);
assert_eq!(h, 1080.0);
}
#[test]
fn test_letterbox_size_matches_aspect() {
let (w, h) = letterbox_size(800.0, 600.0, NTSC_ASPECT);
assert!((w - 731.4286).abs() < 0.01);
assert_eq!(h, 600.0);
}
#[test]
fn test_letterbox_size_zero_height() {
let (w, h) = letterbox_size(800.0, 0.0, NTSC_ASPECT);
assert_eq!(w, 800.0);
assert_eq!(h, 0.0);
}
}
#[cfg(test)]
mod tests_crosshair_projection {
use super::{Crosshair, CrosshairDrawContext, project_crosshair_to_cropped_indices};
#[test]
fn test_crosshair_projection_without_overscan() {
let draw_ctx = CrosshairDrawContext {
x0: 0.0,
y0: 0.0,
draw_w: 256.0,
draw_h: 240.0,
cropped_w: 256,
cropped_h: 240,
h_overscan: 0,
v_overscan: 0,
};
let (ix, iy) =
project_crosshair_to_cropped_indices(Crosshair { x: 10.0, y: 20.0 }, &draw_ctx);
assert_eq!(ix, 10.0);
assert_eq!(iy, 20.0);
}
#[test]
fn test_crosshair_projection_applies_vertical_overscan_offset() {
let draw_ctx = CrosshairDrawContext {
x0: 0.0,
y0: 0.0,
draw_w: 256.0,
draw_h: 224.0,
cropped_w: 256,
cropped_h: 224,
h_overscan: 0,
v_overscan: 8,
};
let (ix, iy) =
project_crosshair_to_cropped_indices(Crosshair { x: 100.0, y: 40.0 }, &draw_ctx);
assert_eq!(ix, 100.0);
assert_eq!(iy, 32.0);
}
#[test]
fn test_crosshair_projection_clamps_to_visible_region() {
let draw_ctx = CrosshairDrawContext {
x0: 0.0,
y0: 0.0,
draw_w: 240.0,
draw_h: 208.0,
cropped_w: 240,
cropped_h: 208,
h_overscan: 8,
v_overscan: 16,
};
let (ix, iy) =
project_crosshair_to_cropped_indices(Crosshair { x: 255.0, y: 239.0 }, &draw_ctx);
assert_eq!(ix, 239.0);
assert_eq!(iy, 207.0);
}
}
#[cfg(test)]
mod tests_egui_frame_input {
use super::{
LineSegment, RegisteredEguiTexture, crosshair_rgba, egui_color_from_rgba,
egui_frame_input_for_window, fps_counter_text, gb_tiles_texture_aspect,
gb_tiles_texture_uv, scroll_rect_line_segments, toast_background_rgba, toast_text_rgba,
};
#[test]
fn egui_frame_input_uses_logical_and_drawable_sizes() {
let frame_input = egui_frame_input_for_window((320, 240), (640, 480), 1.0 / 60.0);
assert_eq!(frame_input.drawable_size(), [640, 480]);
assert_eq!(frame_input.pixels_per_point(), 2.0);
}
#[test]
fn egui_frame_input_falls_back_to_height_scale_when_width_is_zero() {
let frame_input = egui_frame_input_for_window((0, 240), (640, 480), 1.0 / 60.0);
assert_eq!(frame_input.pixels_per_point(), 2.0);
}
#[test]
fn egui_frame_input_uses_unit_scale_for_empty_window() {
let frame_input = egui_frame_input_for_window((0, 0), (640, 480), 1.0 / 60.0);
assert_eq!(frame_input.pixels_per_point(), 1.0);
}
#[test]
fn egui_color_from_rgba_converts_unit_floats_to_color32() {
assert_eq!(
egui_color_from_rgba([1.0, 0.5, 0.0, 0.25]),
egui::Color32::from_rgba_unmultiplied(255, 128, 0, 64)
);
}
#[test]
fn egui_color_from_rgba_clamps_out_of_range_channels() {
assert_eq!(
egui_color_from_rgba([2.0, -1.0, 0.0, 1.5]),
egui::Color32::from_rgba_unmultiplied(255, 0, 0, 255)
);
}
#[test]
fn fps_counter_text_formats_value() {
assert_eq!(fps_counter_text(60), "60 FPS");
}
#[test]
fn toast_egui_colors_match_rgba_helpers() {
assert_eq!(
egui_color_from_rgba(toast_text_rgba()),
egui::Color32::from_rgba_unmultiplied(255, 255, 255, 255)
);
assert_eq!(
egui_color_from_rgba(toast_background_rgba()),
egui::Color32::from_rgba_unmultiplied(89, 89, 89, 179)
);
}
#[test]
fn crosshair_egui_color_matches_rgba_helper() {
assert_eq!(
egui_color_from_rgba(crosshair_rgba()),
egui::Color32::from_rgba_unmultiplied(255, 51, 51, 255)
);
}
#[test]
fn registered_egui_texture_matches_its_gl_id() {
let registered = RegisteredEguiTexture {
gl_id: 42,
egui_id: egui::TextureId::User(7),
};
assert!(registered.matches_gl_id(42));
assert!(!registered.matches_gl_id(7));
}
#[test]
fn scroll_rect_line_segments_draws_unwrapped_rectangle_edges() {
let segments =
scroll_rect_line_segments([10.0, 20.0], (4, 8), (2.0, 3.0), (16.0, 12.0), (64.0, 64.0));
assert_eq!(
segments,
vec![
LineSegment {
start: [18.0, 44.0],
end: [50.0, 44.0],
},
LineSegment {
start: [18.0, 80.0],
end: [50.0, 80.0],
},
LineSegment {
start: [18.0, 44.0],
end: [18.0, 80.0],
},
LineSegment {
start: [50.0, 44.0],
end: [50.0, 80.0],
},
]
);
}
#[test]
fn scroll_rect_line_segments_omits_internal_wrap_edges() {
let segments =
scroll_rect_line_segments([0.0, 0.0], (56, 0), (1.0, 1.0), (16.0, 12.0), (64.0, 64.0));
assert_eq!(
segments,
vec![
LineSegment {
start: [56.0, 0.0],
end: [64.0, 0.0],
},
LineSegment {
start: [56.0, 12.0],
end: [64.0, 12.0],
},
LineSegment {
start: [56.0, 0.0],
end: [56.0, 12.0],
},
LineSegment {
start: [0.0, 0.0],
end: [8.0, 0.0],
},
LineSegment {
start: [0.0, 12.0],
end: [8.0, 12.0],
},
LineSegment {
start: [8.0, 0.0],
end: [8.0, 12.0],
},
]
);
}
#[test]
fn gb_dmg_tiles_image_uses_visible_half_of_cgb_sized_texture() {
assert_eq!(
gb_tiles_texture_uv(false),
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(0.5, 1.0))
);
assert_eq!(gb_tiles_texture_aspect(false), 128.0 / 256.0);
}
#[test]
fn gb_cgb_tiles_image_uses_full_texture() {
assert_eq!(
gb_tiles_texture_uv(true),
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0))
);
assert_eq!(gb_tiles_texture_aspect(true), 128.0 / 512.0);
}
}
unsafe fn create_egui_rgba_texture(
egui_renderer: &mut NativeEguiRenderer,
width: i32,
height: i32,
) -> Result<(gl::types::GLuint, egui::TextureId), String> {
let texture = unsafe { create_rgba_texture_handle(width, height)? };
let texture_name = NativeTextureName::from_gl_id(texture)
.ok_or_else(|| "Failed to create egui RGBA texture".to_owned())?;
let texture_id = egui_renderer.register_native_texture(texture_name);
Ok((texture, texture_id))
}
unsafe fn create_rgba_texture_handle(width: i32, height: i32) -> Result<gl::types::GLuint, String> {
unsafe {
let mut texture: gl::types::GLuint = 0;
gl::GenTextures(1, &mut texture);
if texture == 0 {
return Err(format!("Failed to create RGBA texture {width}x{height}"));
}
gl::BindTexture(gl::TEXTURE_2D, texture);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::NEAREST as i32);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::NEAREST as i32);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32);
gl::PixelStorei(gl::UNPACK_ALIGNMENT, 1);
gl::TexImage2D(
gl::TEXTURE_2D,
0,
gl::RGBA8 as i32,
width,
height,
0,
gl::RGBA,
gl::UNSIGNED_BYTE,
std::ptr::null(),
);
Ok(texture)
}
}
unsafe fn upload_rgba_texture(texture: gl::types::GLuint, width: i32, height: i32, pixels: &[u8]) {
unsafe {
gl::BindTexture(gl::TEXTURE_2D, texture);
gl::PixelStorei(gl::UNPACK_ALIGNMENT, 1);
gl::TexSubImage2D(
gl::TEXTURE_2D,
0,
0,
0,
width,
height,
gl::RGBA,
gl::UNSIGNED_BYTE,
pixels.as_ptr() as *const c_void,
);
}
}
fn draw_ppu_viewer_window(
ui: &mut egui::Ui,
nt_texture_id: egui::TextureId,
tiles_texture_id: egui::TextureId,
scroll: (u16, u16),
) {
const TILES_ASPECT: f32 =
PPU_VIEWER_TILES_TEXTURE_HEIGHT as f32 / PPU_VIEWER_TILES_TEXTURE_WIDTH as f32;
const NT_ASPECT: f32 = PPU_VIEWER_NT_TEXTURE_HEIGHT as f32 / PPU_VIEWER_NT_TEXTURE_WIDTH as f32;
const VISIBLE_W: f32 = 256.0;
const VISIBLE_H: f32 = 240.0;
const NT_TEX_W: f32 = PPU_VIEWER_NT_TEXTURE_WIDTH as f32;
const NT_TEX_H: f32 = PPU_VIEWER_NT_TEXTURE_HEIGHT as f32;
egui::Window::new("PPU Viewer")
.default_size(egui::vec2(
PPU_VIEWER_WINDOW_INITIAL_WIDTH,
PPU_VIEWER_WINDOW_INITIAL_HEIGHT,
))
.show(ui.ctx(), |ui| {
ui.label("Pattern Tables");
ui.separator();
let avail_w = ui.available_width();
ui.add(egui::Image::from_texture(egui::load::SizedTexture::new(
tiles_texture_id,
egui::vec2(avail_w, avail_w * TILES_ASPECT),
)));
ui.add_space(6.0);
ui.label("Nametables (2\u{00D7}2)");
ui.separator();
let avail_w = ui.available_width();
let img_h = avail_w * NT_ASPECT;
let response = ui.add(egui::Image::from_texture(egui::load::SizedTexture::new(
nt_texture_id,
egui::vec2(avail_w, img_h),
)));
let sx = avail_w / NT_TEX_W;
let sy = img_h / NT_TEX_H;
draw_egui_scroll_rect(
ui,
[response.rect.min.x, response.rect.min.y],
scroll,
(sx, sy),
(VISIBLE_W, VISIBLE_H),
(NT_TEX_W, NT_TEX_H),
);
});
}
fn draw_egui_scroll_rect(
ui: &mut egui::Ui,
img_origin: [f32; 2],
scroll: (u16, u16),
scale: (f32, f32),
visible_size: (f32, f32),
nametable_size: (f32, f32),
) {
let stroke = egui::Stroke::new(1.5, egui::Color32::YELLOW);
for segment in
scroll_rect_line_segments(img_origin, scroll, scale, visible_size, nametable_size)
{
ui.painter().line_segment(
[
egui::pos2(segment.start[0], segment.start[1]),
egui::pos2(segment.end[0], segment.end[1]),
],
stroke,
);
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct LineSegment {
start: [f32; 2],
end: [f32; 2],
}
fn scroll_rect_line_segments(
img_origin: [f32; 2],
scroll: (u16, u16),
scale: (f32, f32),
visible_size: (f32, f32),
nametable_size: (f32, f32),
) -> Vec<LineSegment> {
let ox = img_origin[0];
let oy = img_origin[1];
let scroll_x = scroll.0 as f32;
let scroll_y = scroll.1 as f32;
let (sx, sy) = scale;
let (visible_w, visible_h) = visible_size;
let (nt_w, nt_h) = nametable_size;
let x_wraps = scroll_x + visible_w > nt_w;
let x_segs: &[(f32, f32, bool, bool)] = if x_wraps {
let left_w = nt_w - scroll_x;
let right_w = visible_w - left_w;
&[(scroll_x, left_w, true, false), (0.0, right_w, false, true)]
} else {
&[(scroll_x, visible_w, true, true)]
};
let y_wraps = scroll_y + visible_h > nt_h;
let y_segs: &[(f32, f32, bool, bool)] = if y_wraps {
let top_h = nt_h - scroll_y;
let bot_h = visible_h - top_h;
&[(scroll_y, top_h, true, false), (0.0, bot_h, false, true)]
} else {
&[(scroll_y, visible_h, true, true)]
};
let mut segments = Vec::new();
for &(xs, xw, draw_left, draw_right) in x_segs {
for &(ys, yh, draw_top, draw_bottom) in y_segs {
let x0 = ox + xs * sx;
let y0 = oy + ys * sy;
let x1 = x0 + xw * sx;
let y1 = y0 + yh * sy;
if draw_top {
segments.push(LineSegment {
start: [x0, y0],
end: [x1, y0],
});
}
if draw_bottom {
segments.push(LineSegment {
start: [x0, y1],
end: [x1, y1],
});
}
if draw_left {
segments.push(LineSegment {
start: [x0, y0],
end: [x0, y1],
});
}
if draw_right {
segments.push(LineSegment {
start: [x1, y0],
end: [x1, y1],
});
}
}
}
segments
}
fn update_ppu_viewer_textures(
nes: &Nes,
nt_texture: gl::types::GLuint,
tiles_texture: gl::types::GLuint,
) -> (u16, u16) {
let ppu_snap = PpuViewerSnapshot::from_nes(nes);
let nt_pixels = render_nametables_rgba(
&ppu_snap.chr,
&ppu_snap.nametables,
&ppu_snap.palette,
ppu_snap.bg_pattern_table,
ppu_snap.system_palette,
);
let tiles_pixels =
render_pattern_tables_rgba(&ppu_snap.chr, &ppu_snap.palette, ppu_snap.system_palette);
unsafe {
upload_rgba_texture(
nt_texture,
PPU_VIEWER_NT_TEXTURE_WIDTH,
PPU_VIEWER_NT_TEXTURE_HEIGHT,
&nt_pixels,
);
upload_rgba_texture(
tiles_texture,
PPU_VIEWER_TILES_TEXTURE_WIDTH,
PPU_VIEWER_TILES_TEXTURE_HEIGHT,
&tiles_pixels,
);
}
ppu_snap.scroll
}
fn update_gb_ppu_viewer_textures_from_snapshot(
ppu_snap: &crate::gb::debugging::ppu_viewer::GbPpuViewerSnapshot,
tiles_texture: gl::types::GLuint,
bg_maps_texture: gl::types::GLuint,
) {
use crate::gb::debugging::ppu_viewer::{render_bg_maps_rgba, render_tiles_rgba};
let tiles_pixels = render_tiles_rgba(
&ppu_snap.vram,
&ppu_snap.vram_bank1,
ppu_snap.bgp,
&ppu_snap.bg_palette_ram,
ppu_snap.cgb_mode,
);
let bg_maps_pixels = render_bg_maps_rgba(
&ppu_snap.vram,
&ppu_snap.vram_bank1,
ppu_snap.lcdc,
ppu_snap.bgp,
&ppu_snap.bg_palette_ram,
ppu_snap.cgb_mode,
);
let tiles_upload_width = GB_PPU_VIEWER_TILES_TEXTURE_WIDTH_CGB;
let tiles_upload_height = GB_PPU_VIEWER_TILES_TEXTURE_HEIGHT_CGB;
let tiles_upload_pixels = if ppu_snap.cgb_mode {
tiles_pixels
} else {
let src_width = GB_PPU_VIEWER_TILES_TEXTURE_WIDTH_DMG as usize;
let src_height = GB_PPU_VIEWER_TILES_TEXTURE_HEIGHT_DMG as usize;
let dst_width = GB_PPU_VIEWER_TILES_TEXTURE_WIDTH_CGB as usize;
let dst_height = GB_PPU_VIEWER_TILES_TEXTURE_HEIGHT_CGB as usize;
let mut padded_pixels = vec![0u8; dst_width * dst_height * 4];
for row in 0..src_height {
let src_start = row * src_width * 4;
let src_end = src_start + src_width * 4;
let dst_start = row * dst_width * 4;
let dst_end = dst_start + src_width * 4;
padded_pixels[dst_start..dst_end].copy_from_slice(&tiles_pixels[src_start..src_end]);
}
padded_pixels
};
unsafe {
upload_rgba_texture(
tiles_texture,
tiles_upload_width,
tiles_upload_height,
&tiles_upload_pixels,
);
upload_rgba_texture(
bg_maps_texture,
GB_PPU_VIEWER_BG_MAPS_TEXTURE_WIDTH,
GB_PPU_VIEWER_BG_MAPS_TEXTURE_HEIGHT,
&bg_maps_pixels,
);
}
}
fn draw_gb_ppu_viewer_window(
ui: &mut egui::Ui,
tiles_texture_id: egui::TextureId,
bg_maps_texture_id: egui::TextureId,
ppu_snap: &crate::gb::debugging::ppu_viewer::GbPpuViewerSnapshot,
) {
use crate::gb::debugging::ppu_viewer::{format_oam_entries, format_palette_info};
let is_cgb = ppu_snap.cgb_mode;
const BG_MAPS_ASPECT: f32 =
GB_PPU_VIEWER_BG_MAPS_TEXTURE_HEIGHT as f32 / GB_PPU_VIEWER_BG_MAPS_TEXTURE_WIDTH as f32;
egui::Window::new("GB PPU Viewer")
.default_size(egui::vec2(
GB_PPU_VIEWER_WINDOW_INITIAL_WIDTH,
GB_PPU_VIEWER_WINDOW_INITIAL_HEIGHT,
))
.show(ui.ctx(), |ui| {
let mode_label = if is_cgb { "CGB" } else { "DMG" };
ui.label(format!("Tiles ({mode_label})"));
ui.separator();
let avail_w = ui.available_width();
ui.add(
egui::Image::from_texture(egui::load::SizedTexture::new(
tiles_texture_id,
egui::vec2(avail_w, avail_w * gb_tiles_texture_aspect(is_cgb)),
))
.uv(gb_tiles_texture_uv(is_cgb)),
);
ui.add_space(6.0);
ui.label("BG Maps (0x9800 | 0x9C00)");
ui.separator();
let avail_w = ui.available_width();
ui.add(egui::Image::from_texture(egui::load::SizedTexture::new(
bg_maps_texture_id,
egui::vec2(avail_w, avail_w * BG_MAPS_ASPECT),
)));
ui.add_space(6.0);
ui.label("OAM Sprites");
ui.separator();
let oam_lines = format_oam_entries(&ppu_snap.oam, is_cgb);
for line in oam_lines.iter().take(10) {
ui.label(line);
}
if oam_lines.len() > 10 {
ui.label(format!("... {} more sprites", oam_lines.len() - 10));
}
ui.add_space(6.0);
ui.label("Palettes");
ui.separator();
let palette_lines = format_palette_info(
ppu_snap.bgp,
ppu_snap.obp0,
ppu_snap.obp1,
&ppu_snap.bg_palette_ram,
&ppu_snap.obj_palette_ram,
is_cgb,
);
for line in palette_lines {
ui.label(line);
}
});
}
fn gb_tiles_texture_aspect(is_cgb: bool) -> f32 {
if is_cgb {
GB_PPU_VIEWER_TILES_TEXTURE_HEIGHT_CGB as f32 / GB_PPU_VIEWER_TILES_TEXTURE_WIDTH_CGB as f32
} else {
GB_PPU_VIEWER_TILES_TEXTURE_HEIGHT_DMG as f32 / GB_PPU_VIEWER_TILES_TEXTURE_WIDTH_DMG as f32
}
}
fn gb_tiles_texture_uv(is_cgb: bool) -> egui::Rect {
let max_x = if is_cgb { 1.0 } else { 0.5 };
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(max_x, 1.0))
}
impl Drop for GlBackend {
fn drop(&mut self) {
if self.render_target.make_current().is_ok() {
self.egui_renderer.destroy();
}
}
}
#[cfg(test)]
mod tests_notify_resize {
use super::RenderTarget;
struct TrackingMockRenderTarget {
resize_called_with: Option<(u32, u32)>,
}
impl TrackingMockRenderTarget {
fn new() -> Self {
Self {
resize_called_with: None,
}
}
}
impl RenderTarget for TrackingMockRenderTarget {
fn notify_resize(&mut self, w: u32, h: u32) {
self.resize_called_with = Some((w, h));
}
fn window_size(&self) -> (u32, u32) {
(800, 600)
}
fn drawable_size(&self) -> (u32, u32) {
(800, 600)
}
fn swap_buffers(&self) {}
fn make_current(&self) -> Result<(), String> {
Ok(())
}
fn set_fullscreen(&mut self, _enabled: bool) -> Result<(), String> {
Ok(())
}
fn set_mouse_grab(&mut self, _enabled: bool) -> Result<(), String> {
Ok(())
}
}
#[test]
fn test_render_target_notify_resize_records_new_dimensions() {
let mut mock = TrackingMockRenderTarget::new();
mock.notify_resize(1920, 1080);
assert_eq!(mock.resize_called_with, Some((1920, 1080)));
}
#[test]
fn test_render_target_notify_resize_updates_on_each_call() {
let mut mock = TrackingMockRenderTarget::new();
mock.notify_resize(800, 600);
mock.notify_resize(1920, 1080);
assert_eq!(mock.resize_called_with, Some((1920, 1080)));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_overlay_text_color_toggle() {
assert_eq!(OverlayTextColor::White.toggle(), OverlayTextColor::Black);
assert_eq!(OverlayTextColor::Black.toggle(), OverlayTextColor::White);
}
#[test]
fn test_overlay_background_color_is_half_alpha_black() {
assert_eq!(
overlay_background_color_for(OverlayTextColor::White),
[0.0, 0.0, 0.0, 0.5]
);
assert_eq!(
overlay_background_color_for(OverlayTextColor::Black),
[1.0, 1.0, 1.0, 0.5]
);
}
#[test]
fn test_overlay_text_color_blink_red() {
assert_eq!(
overlay_text_rgba(OverlayTextColor::White, true),
[1.0, 0.0, 0.0, 1.0]
);
assert_eq!(
overlay_text_rgba(OverlayTextColor::Black, false),
[0.0, 0.0, 0.0, 1.0]
);
}
#[test]
fn test_shader_toast_message_with_name_shows_shader_name() {
assert_eq!(
super::shader_toast_message(Some("ntsc")),
"Visual Filter: NTSC"
);
}
#[test]
fn test_shader_toast_message_with_none_shows_no_shader() {
assert_eq!(super::shader_toast_message(None), "Visual Filter: off");
}
}