split code for prometeu runner
This commit is contained in:
parent
3fd2574be7
commit
e3c623aea5
11
crates/core/src/fw/firmware.rs
Normal file
11
crates/core/src/fw/firmware.rs
Normal file
@ -0,0 +1,11 @@
|
||||
pub struct Firmware {
|
||||
|
||||
}
|
||||
|
||||
impl Firmware {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
1
crates/core/src/fw/mod.rs
Normal file
1
crates/core/src/fw/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod firmware;
|
||||
@ -1,367 +1,14 @@
|
||||
use pixels::{Pixels, SurfaceTexture};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
mod audio_mixer;
|
||||
mod prometeu_runner;
|
||||
|
||||
use audio_mixer::AudioMixer;
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use prometeu_core::peripherals::{AudioCommand, InputSignals, OUTPUT_SAMPLE_RATE};
|
||||
use prometeu_core::LogicalHardware;
|
||||
use ringbuf::traits::{Consumer, Producer, Split};
|
||||
use ringbuf::HeapRb;
|
||||
use std::sync::Arc;
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
dpi::LogicalSize,
|
||||
event::{ElementState, MouseButton, WindowEvent},
|
||||
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
|
||||
keyboard::{KeyCode, PhysicalKey},
|
||||
window::{Window, WindowAttributes, WindowId},
|
||||
};
|
||||
use crate::prometeu_runner::PrometeuRunner;
|
||||
use winit::event_loop::EventLoop;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let event_loop = EventLoop::new()?;
|
||||
|
||||
let mut app = PrometeuApp::new();
|
||||
event_loop.run_app(&mut app)?;
|
||||
let mut runner = PrometeuRunner::new();
|
||||
event_loop.run_app(&mut runner)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct PrometeuApp {
|
||||
window: Option<&'static Window>,
|
||||
pixels: Option<Pixels<'static>>,
|
||||
|
||||
logical_hardware: LogicalHardware,
|
||||
|
||||
input_signals: InputSignals,
|
||||
|
||||
frame_target_dt: Duration,
|
||||
last_frame_time: Instant,
|
||||
accumulator: Duration,
|
||||
|
||||
last_stats_update: Instant,
|
||||
frames_since_last_update: u64,
|
||||
audio_load_accum_us: u64,
|
||||
audio_load_samples: u64,
|
||||
audio_perf_consumer: Option<ringbuf::wrap::CachingCons<Arc<HeapRb<u64>>>>,
|
||||
|
||||
audio_producer: Option<ringbuf::wrap::CachingProd<Arc<HeapRb<AudioCommand>>>>,
|
||||
_audio_stream: Option<cpal::Stream>,
|
||||
}
|
||||
|
||||
impl PrometeuApp {
|
||||
fn new() -> Self {
|
||||
let target_fps = 60;
|
||||
|
||||
Self {
|
||||
window: None,
|
||||
pixels: None,
|
||||
|
||||
logical_hardware: LogicalHardware::new(),
|
||||
|
||||
input_signals: InputSignals::default(),
|
||||
|
||||
frame_target_dt: Duration::from_nanos(1_000_000_000 / target_fps),
|
||||
last_frame_time: Instant::now(),
|
||||
accumulator: Duration::ZERO,
|
||||
|
||||
last_stats_update: Instant::now(),
|
||||
frames_since_last_update: 0,
|
||||
audio_load_accum_us: 0,
|
||||
audio_load_samples: 0,
|
||||
audio_perf_consumer: None,
|
||||
|
||||
audio_producer: None,
|
||||
_audio_stream: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn init_audio(&mut self) {
|
||||
let host = cpal::default_host();
|
||||
let device = host
|
||||
.default_output_device()
|
||||
.expect("no output device available");
|
||||
|
||||
let config = cpal::StreamConfig {
|
||||
channels: 2,
|
||||
sample_rate: cpal::SampleRate(OUTPUT_SAMPLE_RATE),
|
||||
buffer_size: cpal::BufferSize::Default,
|
||||
};
|
||||
|
||||
let rb = HeapRb::<AudioCommand>::new(1024);
|
||||
let (prod, mut cons) = rb.split();
|
||||
|
||||
self.audio_producer = Some(prod);
|
||||
|
||||
let mut mixer = AudioMixer::new();
|
||||
|
||||
// Para passar dados de performance da thread de áudio para a principal
|
||||
let audio_perf_rb = HeapRb::<u64>::new(64);
|
||||
let (mut perf_prod, perf_cons) = audio_perf_rb.split();
|
||||
|
||||
let stream = device
|
||||
.build_output_stream(
|
||||
&config,
|
||||
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
|
||||
// Consome comandos da ringbuffer
|
||||
while let Some(cmd) = cons.try_pop() {
|
||||
mixer.process_command(cmd);
|
||||
}
|
||||
// Mixa áudio
|
||||
mixer.fill_buffer(data);
|
||||
// Envia tempo de processamento em microssegundos
|
||||
let _ = perf_prod.try_push(mixer.last_processing_time.as_micros() as u64);
|
||||
},
|
||||
|err| eprintln!("audio stream error: {}", err),
|
||||
None,
|
||||
)
|
||||
.expect("failed to build audio stream");
|
||||
|
||||
stream.play().expect("failed to play audio stream");
|
||||
self._audio_stream = Some(stream);
|
||||
self.audio_perf_consumer = Some(perf_cons);
|
||||
}
|
||||
|
||||
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 | GFX: {:.1} KB | FPS: {:.1} | Load: {:.1}% (C) + {:.1}% (A) | Frame: tick {} logical {}",
|
||||
0.0, 0.0, 0.0, 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(LogicalHardware::W as u32, LogicalHardware::H as u32, surface_texture)
|
||||
.expect("failed to create Pixels");
|
||||
|
||||
pixels.frame_mut().fill(0);
|
||||
|
||||
self.pixels = Some(pixels);
|
||||
|
||||
self.init_audio();
|
||||
|
||||
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.logical_hardware.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.b_signal = is_down,
|
||||
KeyCode::KeyW => self.input_signals.x_signal = is_down,
|
||||
KeyCode::KeyS => self.input_signals.y_signal = is_down,
|
||||
KeyCode::KeyQ => self.input_signals.l_signal = is_down,
|
||||
KeyCode::KeyE => self.input_signals.r_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.f_signal = true;
|
||||
}
|
||||
ElementState::Released => {
|
||||
self.input_signals.f_signal = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
||||
let now = Instant::now();
|
||||
let mut frame_delta = now.duration_since(self.last_frame_time);
|
||||
|
||||
// Limitador para evitar a "espiral da morte" se o SO travar (máximo de 100ms por volta)
|
||||
if frame_delta > Duration::from_millis(100) {
|
||||
frame_delta = Duration::from_millis(100);
|
||||
}
|
||||
|
||||
self.last_frame_time = now;
|
||||
self.accumulator += frame_delta;
|
||||
|
||||
// 🔥 O coração do determinismo: consome o tempo em fatias exatas de 60Hz
|
||||
while self.accumulator >= self.frame_target_dt {
|
||||
self.logical_hardware.step_frame(&self.input_signals);
|
||||
|
||||
// Envia comandos de áudio gerados neste frame para a thread de áudio
|
||||
if let Some(producer) = &mut self.audio_producer {
|
||||
for cmd in self.logical_hardware.audio.commands.drain(..) {
|
||||
let _ = producer.try_push(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
self.accumulator -= self.frame_target_dt;
|
||||
self.frames_since_last_update += 1;
|
||||
}
|
||||
|
||||
// Drena tempos de performance do áudio
|
||||
if let Some(cons) = &mut self.audio_perf_consumer {
|
||||
while let Some(us) = cons.try_pop() {
|
||||
self.audio_load_accum_us += us;
|
||||
self.audio_load_samples += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Atualiza estatísticas a cada 1 segundo real
|
||||
let stats_elapsed = now.duration_since(self.last_stats_update);
|
||||
if stats_elapsed >= Duration::from_secs(1) {
|
||||
if let Some(window) = self.window {
|
||||
let fps = self.frames_since_last_update as f64 / stats_elapsed.as_secs_f64();
|
||||
let kb = self.logical_hardware.gfx.memory_usage_bytes() as f64 / 1024.0;
|
||||
|
||||
// comparação fixa sempre contra 60Hz, manter mesmo quando fazer teste de stress na CPU
|
||||
let frame_budget_us = 16666.0;
|
||||
let cpu_load_core = (self.logical_hardware.last_frame_cpu_time_us as f64 / frame_budget_us) * 100.0;
|
||||
|
||||
let cpu_load_audio = if self.audio_load_samples > 0 {
|
||||
// O load real é (tempo total processando) / (tempo total de parede).
|
||||
(self.audio_load_accum_us as f64 / stats_elapsed.as_micros() as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let title = format!(
|
||||
"PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Load: {:.1}% (C) + {:.1}% (A) | Frame: tick {} logical {}",
|
||||
kb, fps, cpu_load_core, cpu_load_audio, self.logical_hardware.tick_index, self.logical_hardware.logical_frame_index
|
||||
);
|
||||
window.set_title(&title);
|
||||
}
|
||||
|
||||
self.last_stats_update = now;
|
||||
self.frames_since_last_update = 0;
|
||||
self.audio_load_accum_us = 0;
|
||||
self.audio_load_samples = 0;
|
||||
}
|
||||
|
||||
self.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 = LogicalHardware::W as f32;
|
||||
let fb_h = LogicalHardware::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, LogicalHardware::W as i32 - 1), y.clamp(0, LogicalHardware::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)
|
||||
}
|
||||
}
|
||||
353
crates/host_desktop/src/prometeu_runner.rs
Normal file
353
crates/host_desktop/src/prometeu_runner.rs
Normal file
@ -0,0 +1,353 @@
|
||||
use crate::audio_mixer::AudioMixer;
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use pixels::{Pixels, SurfaceTexture};
|
||||
use prometeu_core::peripherals::{AudioCommand, InputSignals, OUTPUT_SAMPLE_RATE};
|
||||
use prometeu_core::LogicalHardware;
|
||||
use ringbuf::traits::{Consumer, Producer, Split};
|
||||
use ringbuf::HeapRb;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::dpi::LogicalSize;
|
||||
use winit::event::{ElementState, MouseButton, WindowEvent};
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow};
|
||||
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||
use winit::window::{Window, WindowAttributes, WindowId};
|
||||
|
||||
pub struct PrometeuRunner {
|
||||
window: Option<&'static Window>,
|
||||
pixels: Option<Pixels<'static>>,
|
||||
|
||||
logical_hardware: LogicalHardware,
|
||||
|
||||
input_signals: InputSignals,
|
||||
|
||||
frame_target_dt: Duration,
|
||||
last_frame_time: Instant,
|
||||
accumulator: Duration,
|
||||
|
||||
last_stats_update: Instant,
|
||||
frames_since_last_update: u64,
|
||||
audio_load_accum_us: u64,
|
||||
audio_load_samples: u64,
|
||||
audio_perf_consumer: Option<ringbuf::wrap::CachingCons<Arc<HeapRb<u64>>>>,
|
||||
|
||||
audio_producer: Option<ringbuf::wrap::CachingProd<Arc<HeapRb<AudioCommand>>>>,
|
||||
_audio_stream: Option<cpal::Stream>,
|
||||
}
|
||||
|
||||
impl PrometeuRunner {
|
||||
pub(crate) fn new() -> Self {
|
||||
let target_fps = 60;
|
||||
|
||||
Self {
|
||||
window: None,
|
||||
pixels: None,
|
||||
|
||||
logical_hardware: LogicalHardware::new(),
|
||||
|
||||
input_signals: InputSignals::default(),
|
||||
|
||||
frame_target_dt: Duration::from_nanos(1_000_000_000 / target_fps),
|
||||
last_frame_time: Instant::now(),
|
||||
accumulator: Duration::ZERO,
|
||||
|
||||
last_stats_update: Instant::now(),
|
||||
frames_since_last_update: 0,
|
||||
audio_load_accum_us: 0,
|
||||
audio_load_samples: 0,
|
||||
audio_perf_consumer: None,
|
||||
|
||||
audio_producer: None,
|
||||
_audio_stream: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn init_audio(&mut self) {
|
||||
let host = cpal::default_host();
|
||||
let device = host
|
||||
.default_output_device()
|
||||
.expect("no output device available");
|
||||
|
||||
let config = cpal::StreamConfig {
|
||||
channels: 2,
|
||||
sample_rate: cpal::SampleRate(OUTPUT_SAMPLE_RATE),
|
||||
buffer_size: cpal::BufferSize::Default,
|
||||
};
|
||||
|
||||
let rb = HeapRb::<AudioCommand>::new(1024);
|
||||
let (prod, mut cons) = rb.split();
|
||||
|
||||
self.audio_producer = Some(prod);
|
||||
|
||||
let mut mixer = AudioMixer::new();
|
||||
|
||||
// Para passar dados de performance da thread de áudio para a principal
|
||||
let audio_perf_rb = HeapRb::<u64>::new(64);
|
||||
let (mut perf_prod, perf_cons) = audio_perf_rb.split();
|
||||
|
||||
let stream = device
|
||||
.build_output_stream(
|
||||
&config,
|
||||
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
|
||||
// Consome comandos da ringbuffer
|
||||
while let Some(cmd) = cons.try_pop() {
|
||||
mixer.process_command(cmd);
|
||||
}
|
||||
// Mixa áudio
|
||||
mixer.fill_buffer(data);
|
||||
// Envia tempo de processamento em microssegundos
|
||||
let _ = perf_prod.try_push(mixer.last_processing_time.as_micros() as u64);
|
||||
},
|
||||
|err| eprintln!("audio stream error: {}", err),
|
||||
None,
|
||||
)
|
||||
.expect("failed to build audio stream");
|
||||
|
||||
stream.play().expect("failed to play audio stream");
|
||||
self._audio_stream = Some(stream);
|
||||
self.audio_perf_consumer = Some(perf_cons);
|
||||
}
|
||||
|
||||
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 PrometeuRunner {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let attrs = WindowAttributes::default()
|
||||
.with_title(format!(
|
||||
"PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Load: {:.1}% (C) + {:.1}% (A) | Frame: tick {} logical {}",
|
||||
0.0, 0.0, 0.0, 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(LogicalHardware::W as u32, LogicalHardware::H as u32, surface_texture)
|
||||
.expect("failed to create Pixels");
|
||||
|
||||
pixels.frame_mut().fill(0);
|
||||
|
||||
self.pixels = Some(pixels);
|
||||
|
||||
self.init_audio();
|
||||
|
||||
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.logical_hardware.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.b_signal = is_down,
|
||||
KeyCode::KeyW => self.input_signals.x_signal = is_down,
|
||||
KeyCode::KeyS => self.input_signals.y_signal = is_down,
|
||||
KeyCode::KeyQ => self.input_signals.l_signal = is_down,
|
||||
KeyCode::KeyE => self.input_signals.r_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.f_signal = true;
|
||||
}
|
||||
ElementState::Released => {
|
||||
self.input_signals.f_signal = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
||||
let now = Instant::now();
|
||||
let mut frame_delta = now.duration_since(self.last_frame_time);
|
||||
|
||||
// Limitador para evitar a "espiral da morte" se o SO travar (máximo de 100ms por volta)
|
||||
if frame_delta > Duration::from_millis(100) {
|
||||
frame_delta = Duration::from_millis(100);
|
||||
}
|
||||
|
||||
self.last_frame_time = now;
|
||||
self.accumulator += frame_delta;
|
||||
|
||||
// 🔥 O coração do determinismo: consome o tempo em fatias exatas de 60Hz
|
||||
while self.accumulator >= self.frame_target_dt {
|
||||
self.logical_hardware.step_frame(&self.input_signals);
|
||||
|
||||
// Envia comandos de áudio gerados neste frame para a thread de áudio
|
||||
if let Some(producer) = &mut self.audio_producer {
|
||||
for cmd in self.logical_hardware.audio.commands.drain(..) {
|
||||
let _ = producer.try_push(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
self.accumulator -= self.frame_target_dt;
|
||||
self.frames_since_last_update += 1;
|
||||
}
|
||||
|
||||
// Drena tempos de performance do áudio
|
||||
if let Some(cons) = &mut self.audio_perf_consumer {
|
||||
while let Some(us) = cons.try_pop() {
|
||||
self.audio_load_accum_us += us;
|
||||
self.audio_load_samples += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Atualiza estatísticas a cada 1 segundo real
|
||||
let stats_elapsed = now.duration_since(self.last_stats_update);
|
||||
if stats_elapsed >= Duration::from_secs(1) {
|
||||
if let Some(window) = self.window {
|
||||
let fps = self.frames_since_last_update as f64 / stats_elapsed.as_secs_f64();
|
||||
let kb = self.logical_hardware.gfx.memory_usage_bytes() as f64 / 1024.0;
|
||||
|
||||
// comparação fixa sempre contra 60Hz, manter mesmo quando fazer teste de stress na CPU
|
||||
let frame_budget_us = 16666.0;
|
||||
let cpu_load_core = (self.logical_hardware.last_frame_cpu_time_us as f64 / frame_budget_us) * 100.0;
|
||||
|
||||
let cpu_load_audio = if self.audio_load_samples > 0 {
|
||||
// O load real é (tempo total processando) / (tempo total de parede).
|
||||
(self.audio_load_accum_us as f64 / stats_elapsed.as_micros() as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let title = format!(
|
||||
"PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Load: {:.1}% (C) + {:.1}% (A) | Frame: tick {} logical {}",
|
||||
kb, fps, cpu_load_core, cpu_load_audio, self.logical_hardware.tick_index, self.logical_hardware.logical_frame_index
|
||||
);
|
||||
window.set_title(&title);
|
||||
}
|
||||
|
||||
self.last_stats_update = now;
|
||||
self.frames_since_last_update = 0;
|
||||
self.audio_load_accum_us = 0;
|
||||
self.audio_load_samples = 0;
|
||||
}
|
||||
|
||||
self.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 = LogicalHardware::W as f32;
|
||||
let fb_h = LogicalHardware::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, LogicalHardware::W as i32 - 1), y.clamp(0, LogicalHardware::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)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user