From 28ef290d54ba602a77ebdc44ba44a8a87f212acf Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 16 Jan 2026 08:47:58 +0000 Subject: [PATCH] split code for prometeu runner --- crates/core/src/fw/firmware.rs | 11 + crates/core/src/fw/mod.rs | 1 + crates/host_desktop/src/main.rs | 365 +-------------------- crates/host_desktop/src/prometeu_runner.rs | 353 ++++++++++++++++++++ 4 files changed, 371 insertions(+), 359 deletions(-) create mode 100644 crates/core/src/fw/firmware.rs create mode 100644 crates/core/src/fw/mod.rs create mode 100644 crates/host_desktop/src/prometeu_runner.rs diff --git a/crates/core/src/fw/firmware.rs b/crates/core/src/fw/firmware.rs new file mode 100644 index 00000000..3032a967 --- /dev/null +++ b/crates/core/src/fw/firmware.rs @@ -0,0 +1,11 @@ +pub struct Firmware { + +} + +impl Firmware { + pub fn new() -> Self { + Self { + + } + } +} \ No newline at end of file diff --git a/crates/core/src/fw/mod.rs b/crates/core/src/fw/mod.rs new file mode 100644 index 00000000..75b0b1cc --- /dev/null +++ b/crates/core/src/fw/mod.rs @@ -0,0 +1 @@ +pub mod firmware; \ No newline at end of file diff --git a/crates/host_desktop/src/main.rs b/crates/host_desktop/src/main.rs index 31a25926..cdf2241d 100644 --- a/crates/host_desktop/src/main.rs +++ b/crates/host_desktop/src/main.rs @@ -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> { 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>, - - 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>>>, - - audio_producer: Option>>>, - _audio_stream: Option, -} - -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::::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::::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) -} +} \ No newline at end of file diff --git a/crates/host_desktop/src/prometeu_runner.rs b/crates/host_desktop/src/prometeu_runner.rs new file mode 100644 index 00000000..865ffe52 --- /dev/null +++ b/crates/host_desktop/src/prometeu_runner.rs @@ -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>, + + 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>>>, + + audio_producer: Option>>>, + _audio_stream: Option, +} + +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::::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::::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) +} \ No newline at end of file