254 lines
8.5 KiB
Rust
254 lines
8.5 KiB
Rust
use std::time::{Duration, Instant};
|
|
|
|
use pixels::{Pixels, SurfaceTexture};
|
|
use prometeu_core::peripherals::InputSignals;
|
|
use prometeu_core::Machine;
|
|
use winit::{
|
|
application::ApplicationHandler,
|
|
dpi::LogicalSize,
|
|
event::{ElementState, MouseButton, WindowEvent},
|
|
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
|
|
keyboard::{KeyCode, PhysicalKey},
|
|
window::{Window, WindowAttributes, WindowId},
|
|
};
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let event_loop = EventLoop::new()?;
|
|
|
|
let mut app = PrometeuApp::new();
|
|
event_loop.run_app(&mut app)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
struct PrometeuApp {
|
|
window: Option<&'static Window>,
|
|
pixels: Option<Pixels<'static>>,
|
|
|
|
machine: Machine,
|
|
|
|
input_signals: InputSignals,
|
|
|
|
frame_target_dt: Duration,
|
|
next_frame: Instant,
|
|
}
|
|
|
|
impl PrometeuApp {
|
|
fn new() -> Self {
|
|
Self {
|
|
window: None,
|
|
pixels: None,
|
|
|
|
machine: Machine::new(),
|
|
|
|
input_signals: InputSignals::default(),
|
|
|
|
frame_target_dt: Duration::from_nanos(1_000_000_000 / 60),
|
|
next_frame: Instant::now(),
|
|
}
|
|
}
|
|
|
|
fn window(&self) -> &'static Window {
|
|
self.window.expect("window not created yet")
|
|
}
|
|
|
|
// fn pixels_mut(&mut self) -> &mut Pixels<'static> {
|
|
// self.pixels.as_mut().expect("pixels not created yet")
|
|
// }
|
|
|
|
fn resize_surface(&mut self, width: u32, height: u32) {
|
|
if let Some(p) = self.pixels.as_mut() {
|
|
let _ = p.resize_surface(width, height);
|
|
}
|
|
}
|
|
|
|
fn request_redraw(&self) {
|
|
if let Some(w) = self.window.as_ref() {
|
|
w.request_redraw();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ApplicationHandler for PrometeuApp {
|
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
|
let attrs = WindowAttributes::default()
|
|
.with_title(format!("PROMETEU - host_desktop | GFX Mem: {:.2} KB | Frame: {}", 0.0, 0))
|
|
.with_inner_size(LogicalSize::new(960.0, 540.0))
|
|
.with_min_inner_size(LogicalSize::new(320.0, 180.0));
|
|
|
|
let window = event_loop.create_window(attrs).expect("failed to create window");
|
|
|
|
// 🔥 Leak: Window vira &'static Window (bootstrap)
|
|
let window: &'static Window = Box::leak(Box::new(window));
|
|
self.window = Some(window);
|
|
|
|
let size = window.inner_size();
|
|
let surface_texture = SurfaceTexture::new(size.width, size.height, window);
|
|
|
|
let mut pixels = Pixels::new(Machine::W as u32, Machine::H as u32, surface_texture)
|
|
.expect("failed to create Pixels");
|
|
|
|
pixels.frame_mut().fill(0);
|
|
|
|
self.pixels = Some(pixels);
|
|
|
|
self.next_frame = Instant::now();
|
|
event_loop.set_control_flow(ControlFlow::Poll);
|
|
}
|
|
|
|
|
|
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
|
|
match event {
|
|
WindowEvent::CloseRequested => event_loop.exit(),
|
|
|
|
WindowEvent::Resized(size) => {
|
|
self.resize_surface(size.width, size.height);
|
|
}
|
|
|
|
WindowEvent::ScaleFactorChanged { .. } => {
|
|
let size = self.window().inner_size();
|
|
self.resize_surface(size.width, size.height);
|
|
}
|
|
|
|
WindowEvent::RedrawRequested => {
|
|
// Pegue o Pixels diretamente do campo (não via helper que pega &mut self inteiro)
|
|
let pixels = self.pixels.as_mut().expect("pixels not initialized");
|
|
|
|
{
|
|
// Borrow mutável do frame (dura só dentro deste bloco)
|
|
let frame = pixels.frame_mut();
|
|
|
|
// Borrow imutável do core (campo diferente, ok)
|
|
let src = self.machine.gfx.front_buffer();
|
|
|
|
draw_rgb565_to_rgba8(src, frame);
|
|
} // <- frame borrow termina aqui
|
|
|
|
if pixels.render().is_err() {
|
|
event_loop.exit();
|
|
}
|
|
}
|
|
|
|
|
|
WindowEvent::KeyboardInput { event, .. } => {
|
|
if let PhysicalKey::Code(code) = event.physical_key {
|
|
let is_down = event.state == ElementState::Pressed;
|
|
match code {
|
|
KeyCode::ArrowUp => self.input_signals.up_signal = is_down,
|
|
KeyCode::ArrowDown => self.input_signals.down_signal = is_down,
|
|
KeyCode::ArrowLeft => self.input_signals.left_signal = is_down,
|
|
KeyCode::ArrowRight => self.input_signals.right_signal = is_down,
|
|
|
|
// A/B (troque depois como quiser)
|
|
KeyCode::KeyA => self.input_signals.a_signal = is_down,
|
|
KeyCode::KeyD => self.input_signals.d_signal = is_down,
|
|
KeyCode::KeyW => self.input_signals.w_signal = is_down,
|
|
KeyCode::KeyS => self.input_signals.s_signal = is_down,
|
|
KeyCode::KeyQ => self.input_signals.q_signal = is_down,
|
|
KeyCode::KeyE => self.input_signals.e_signal = is_down,
|
|
|
|
KeyCode::KeyZ => self.input_signals.start_signal = is_down,
|
|
KeyCode::ShiftLeft | KeyCode::ShiftRight => self.input_signals.select_signal = is_down,
|
|
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
WindowEvent::CursorMoved { position, .. } => {
|
|
let v = window_to_fb(position.x as f32, position.y as f32, self.window());
|
|
self.input_signals.x_pos = v.0;
|
|
self.input_signals.y_pos = v.1;
|
|
}
|
|
|
|
WindowEvent::MouseInput { state, button, .. } => {
|
|
if button == MouseButton::Left {
|
|
match state {
|
|
ElementState::Pressed => {
|
|
self.input_signals.t_signal = true;
|
|
}
|
|
ElementState::Released => {
|
|
self.input_signals.t_signal = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
|
// Pacing simples a ~60Hz
|
|
let now = Instant::now();
|
|
|
|
if now >= self.next_frame {
|
|
self.next_frame += self.frame_target_dt;
|
|
|
|
// executa 1 frame do PROMETEU
|
|
self.machine.step_frame(&self.input_signals);
|
|
|
|
// temp: atualiza o título da janela a cada 1 segundo (60 frames)
|
|
if self.machine.frame_index % 60 == 0 {
|
|
if let Some(window) = self.window {
|
|
let usage_bytes = self.machine.gfx.memory_usage_bytes();
|
|
let kb = usage_bytes as f64 / 1024.0;
|
|
|
|
let title = format!(
|
|
"PROMETEU - host_desktop | GFX Mem: {:.2} KB | Frame: {}",
|
|
kb,
|
|
self.machine.frame_index);
|
|
window.set_title(&title);
|
|
}
|
|
}
|
|
|
|
// pede redraw
|
|
self.request_redraw();
|
|
} else {
|
|
// dorme pouco para não fritar CPU
|
|
let sleep_for = self.next_frame - now;
|
|
std::thread::sleep(sleep_for.min(Duration::from_millis(2)));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Mapeamento simples: window coords -> framebuffer coords (stretch).
|
|
/// Depois podemos fazer letterbox/aspect-ratio correto.
|
|
fn window_to_fb(wx: f32, wy: f32, window: &Window) -> (i32, i32) {
|
|
let size = window.inner_size();
|
|
let fb_w = Machine::W as f32;
|
|
let fb_h = Machine::H as f32;
|
|
|
|
let x = (wx * fb_w / size.width as f32).floor() as i32;
|
|
let y = (wy * fb_h / size.height as f32).floor() as i32;
|
|
|
|
(x.clamp(0, Machine::W as i32 - 1), y.clamp(0, Machine::H as i32 - 1))
|
|
}
|
|
|
|
/// Copia RGB565 (u16) -> RGBA8888 (u8[4]) para o frame do pixels.
|
|
/// Formato do pixels: RGBA8.
|
|
fn draw_rgb565_to_rgba8(src: &[u16], dst_rgba: &mut [u8]) {
|
|
for (i, &px) in src.iter().enumerate() {
|
|
let (r8, g8, b8) = rgb565_to_rgb888(px);
|
|
let o = i * 4;
|
|
dst_rgba[o] = r8;
|
|
dst_rgba[o + 1] = g8;
|
|
dst_rgba[o + 2] = b8;
|
|
dst_rgba[o + 3] = 0xFF;
|
|
}
|
|
}
|
|
|
|
/// Expande RGB565 para RGB888 (replicação de bits altos).
|
|
#[inline(always)]
|
|
fn rgb565_to_rgb888(px: u16) -> (u8, u8, u8) {
|
|
let r5 = ((px >> 11) & 0x1F) as u8;
|
|
let g6 = ((px >> 5) & 0x3F) as u8;
|
|
let b5 = (px & 0x1F) as u8;
|
|
|
|
let r8 = (r5 << 3) | (r5 >> 2);
|
|
let g8 = (g6 << 2) | (g6 >> 4);
|
|
let b8 = (b5 << 3) | (b5 >> 2);
|
|
|
|
(r8, g8, b8)
|
|
}
|