first window
This commit is contained in:
parent
ba673b4547
commit
4ccd38580f
2344
Cargo.lock
generated
2344
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
28
crates/core/src/color.rs
Normal file
28
crates/core/src/color.rs
Normal file
@ -0,0 +1,28 @@
|
||||
/// Cor simples em RGB565 (0bRRRRRGGGGGGBBBBB).
|
||||
/// - 5 bits Red
|
||||
/// - 6 bits Green
|
||||
/// - 5 bits Blue
|
||||
///
|
||||
/// Não há canal alpha.
|
||||
/// Transparência é tratada via Color Key ou Blend Mode.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Color(pub u16);
|
||||
|
||||
impl Color {
|
||||
/// Cria uma cor RGB565 a partir de componentes 8-bit.
|
||||
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
let r5 = (r as u16 >> 3) & 0x1F;
|
||||
let g6 = (g as u16 >> 2) & 0x3F;
|
||||
let b5 = (b as u16 >> 3) & 0x1F;
|
||||
|
||||
Self((r5 << 11) | (g6 << 5) | b5)
|
||||
}
|
||||
|
||||
pub const fn from_raw(raw: u16) -> Self {
|
||||
Self(raw)
|
||||
}
|
||||
|
||||
pub const fn raw(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
pub mod machine;
|
||||
pub mod peripherals;
|
||||
mod color;
|
||||
|
||||
pub use machine::Machine;
|
||||
pub use color::Color;
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
use crate::peripherals::{Gfx, Input, Touch};
|
||||
use crate::Color;
|
||||
|
||||
/// PROMETEU "hardware lógico" (v0).
|
||||
/// O host alimenta INPUT/TOUCH e chama `tick()` em 60Hz.
|
||||
/// O Host alimenta INPUT/TOUCH e chama `step_frame()` em 60Hz.
|
||||
pub struct Machine {
|
||||
pub gfx: Gfx,
|
||||
pub input: Input,
|
||||
@ -22,44 +23,47 @@ impl Machine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Um frame lógico do PROMETEU.
|
||||
/// (Depois isso chamará um "cartucho"; por enquanto é demo hardcoded.)
|
||||
pub fn tick(&mut self) {
|
||||
/// "Contrato de frame" do PROMETEU (Template Method sem loop).
|
||||
/// O Host controla tempo/event loop; o Core define a sequência do frame.
|
||||
pub fn step_frame(&mut self) {
|
||||
self.begin_frame();
|
||||
self.tick(); // futuro: executa cartucho/VM
|
||||
self.end_frame(); // present/housekeeping
|
||||
}
|
||||
|
||||
/// Início do frame: zera flags transitórias.
|
||||
pub fn begin_frame(&mut self) {
|
||||
self.frame_index += 1;
|
||||
|
||||
// Limpa
|
||||
// Flags transitórias do TOUCH devem durar 1 frame.
|
||||
self.touch.begin_frame();
|
||||
|
||||
// Se você quiser input com pressed/released depois:
|
||||
// self.input.begin_frame();
|
||||
}
|
||||
|
||||
/// Lógica do frame (demo hardcoded por enquanto).
|
||||
pub fn tick(&mut self) {
|
||||
self.gfx.clear(Color::rgb(0x10, 0x10, 0x10));
|
||||
|
||||
// Demo: quadrado que muda de cor se A ou touch estiver pressionado
|
||||
let (x, y) = self.demo_pos();
|
||||
|
||||
let color = if self.input.a_down || self.touch.down {
|
||||
Color::rgb(0x00, 0xFF, 0x00)
|
||||
} else {
|
||||
Color::rgb(0xFF, 0x40, 0x40)
|
||||
};
|
||||
self.gfx.fill_rect(x, y, 24, 24, color);
|
||||
|
||||
// Present (double buffer)
|
||||
self.gfx.fill_rect(x, y, 24, 24, color);
|
||||
}
|
||||
|
||||
/// Final do frame: troca buffers.
|
||||
pub fn end_frame(&mut self) {
|
||||
self.gfx.present();
|
||||
}
|
||||
|
||||
fn demo_pos(&self) -> (i32, i32) {
|
||||
// Só para mostrar movimento sem input por enquanto.
|
||||
let t = (self.frame_index % 280) as i32;
|
||||
(20 + t, 80)
|
||||
}
|
||||
}
|
||||
|
||||
/// Cor simples em RGBA8888 (0xRRGGBBAA).
|
||||
/// (Mais tarde, para hardware barato, você pode mudar o formato para RGB565.)
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Color(pub u32);
|
||||
|
||||
impl Color {
|
||||
pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self(((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | (a as u32))
|
||||
}
|
||||
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
Self::rgba(r, g, b, 0xFF)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,25 @@
|
||||
use crate::machine::Color;
|
||||
use crate::color::Color;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum BlendMode {
|
||||
/// dst = src
|
||||
#[default]
|
||||
None,
|
||||
/// dst = (src + dst) / 2
|
||||
Half,
|
||||
/// dst = dst + (src / 2)
|
||||
HalfPlus,
|
||||
/// dst = dst - (src / 2)
|
||||
HalfMinus,
|
||||
/// dst = dst + src
|
||||
Full,
|
||||
}
|
||||
|
||||
pub struct Gfx {
|
||||
w: usize,
|
||||
h: usize,
|
||||
front: Vec<u32>,
|
||||
back: Vec<u32>,
|
||||
front: Vec<u16>,
|
||||
back: Vec<u16>,
|
||||
}
|
||||
|
||||
impl Gfx {
|
||||
@ -22,8 +37,8 @@ impl Gfx {
|
||||
(self.w, self.h)
|
||||
}
|
||||
|
||||
/// O buffer que o host deve exibir.
|
||||
pub fn front_buffer(&self) -> &[u32] {
|
||||
/// O buffer que o host deve exibir (RGB565).
|
||||
pub fn front_buffer(&self) -> &[u16] {
|
||||
&self.front
|
||||
}
|
||||
|
||||
@ -31,7 +46,16 @@ impl Gfx {
|
||||
self.back.fill(color.0);
|
||||
}
|
||||
|
||||
pub fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
|
||||
/// Retângulo com modo de blend.
|
||||
pub fn fill_rect_blend(
|
||||
&mut self,
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: i32,
|
||||
h: i32,
|
||||
color: Color,
|
||||
mode: BlendMode,
|
||||
) {
|
||||
let fw = self.w as i32;
|
||||
let fh = self.h as i32;
|
||||
|
||||
@ -40,16 +64,90 @@ impl Gfx {
|
||||
let x1 = (x + w).clamp(0, fw);
|
||||
let y1 = (y + h).clamp(0, fh);
|
||||
|
||||
let src = color.0;
|
||||
|
||||
for yy in y0..y1 {
|
||||
let row = (yy as usize) * self.w;
|
||||
for xx in x0..x1 {
|
||||
self.back[row + (xx as usize)] = color.0;
|
||||
let idx = row + (xx as usize);
|
||||
let dst = self.back[idx];
|
||||
self.back[idx] = blend_rgb565(dst, src, mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Double buffer swap.
|
||||
/// Conveniência: retângulo normal (sem blend).
|
||||
pub fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
|
||||
self.fill_rect_blend(x, y, w, h, color, BlendMode::None);
|
||||
}
|
||||
|
||||
/// Double buffer swap (O(1), sem cópia de pixels).
|
||||
pub fn present(&mut self) {
|
||||
std::mem::swap(&mut self.front, &mut self.back);
|
||||
}
|
||||
}
|
||||
|
||||
/// Faz blend em RGB565 por canal com saturação.
|
||||
/// `dst` e `src` são pixels RGB565 (u16).
|
||||
fn blend_rgb565(dst: u16, src: u16, mode: BlendMode) -> u16 {
|
||||
match mode {
|
||||
BlendMode::None => src,
|
||||
|
||||
BlendMode::Half => {
|
||||
let (dr, dg, db) = unpack_rgb565(dst);
|
||||
let (sr, sg, sb) = unpack_rgb565(src);
|
||||
let r = ((dr as u16 + sr as u16) >> 1) as u8;
|
||||
let g = ((dg as u16 + sg as u16) >> 1) as u8;
|
||||
let b = ((db as u16 + sb as u16) >> 1) as u8;
|
||||
pack_rgb565(r, g, b)
|
||||
}
|
||||
|
||||
BlendMode::HalfPlus => {
|
||||
let (dr, dg, db) = unpack_rgb565(dst);
|
||||
let (sr, sg, sb) = unpack_rgb565(src);
|
||||
|
||||
let r = (dr as u16 + ((sr as u16) >> 1)).min(31) as u8;
|
||||
let g = (dg as u16 + ((sg as u16) >> 1)).min(63) as u8;
|
||||
let b = (db as u16 + ((sb as u16) >> 1)).min(31) as u8;
|
||||
|
||||
pack_rgb565(r, g, b)
|
||||
}
|
||||
|
||||
BlendMode::HalfMinus => {
|
||||
let (dr, dg, db) = unpack_rgb565(dst);
|
||||
let (sr, sg, sb) = unpack_rgb565(src);
|
||||
|
||||
let r = (dr as i16 - ((sr as i16) >> 1)).max(0) as u8;
|
||||
let g = (dg as i16 - ((sg as i16) >> 1)).max(0) as u8;
|
||||
let b = (db as i16 - ((sb as i16) >> 1)).max(0) as u8;
|
||||
|
||||
pack_rgb565(r, g, b)
|
||||
}
|
||||
|
||||
BlendMode::Full => {
|
||||
let (dr, dg, db) = unpack_rgb565(dst);
|
||||
let (sr, sg, sb) = unpack_rgb565(src);
|
||||
|
||||
let r = (dr as u16 + sr as u16).min(31) as u8;
|
||||
let g = (dg as u16 + sg as u16).min(63) as u8;
|
||||
let b = (db as u16 + sb as u16).min(31) as u8;
|
||||
|
||||
pack_rgb565(r, g, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrai canais já na faixa nativa do RGB565:
|
||||
/// R: 0..31, G: 0..63, B: 0..31
|
||||
#[inline(always)]
|
||||
fn unpack_rgb565(px: u16) -> (u8, u8, u8) {
|
||||
let r = ((px >> 11) & 0x1F) as u8;
|
||||
let g = ((px >> 5) & 0x3F) as u8;
|
||||
let b = (px & 0x1F) as u8;
|
||||
(r, g, b)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn pack_rgb565(r: u8, g: u8, b: u8) -> u16 {
|
||||
((r as u16 & 0x1F) << 11) | ((g as u16 & 0x3F) << 5) | (b as u16 & 0x1F)
|
||||
}
|
||||
|
||||
@ -11,3 +11,10 @@ pub struct Input {
|
||||
pub start: bool,
|
||||
pub select: bool,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
pub fn begin_frame(&mut self) {
|
||||
// No v0, nada a fazer.
|
||||
// No futuro: a_pressed/a_released, etc.
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,3 +7,11 @@ pub struct Touch {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl Touch {
|
||||
/// Flags transitórias devem durar apenas 1 frame.
|
||||
pub fn begin_frame(&mut self) {
|
||||
self.pressed = false;
|
||||
self.released = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
[package]
|
||||
name = "host_desktop"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
prometeu-core = { path = "../core" }
|
||||
winit = "0.30.12"
|
||||
pixels = "0.15.0"
|
||||
@ -1,3 +1,264 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use pixels::{Pixels, SurfaceTexture};
|
||||
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,
|
||||
|
||||
frame_dt: Duration,
|
||||
next_frame: Instant,
|
||||
|
||||
mouse_down: bool,
|
||||
mouse_just_pressed: bool,
|
||||
mouse_just_released: bool,
|
||||
mouse_pos_fb: (i32, i32),
|
||||
}
|
||||
|
||||
impl PrometeuApp {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
window: None,
|
||||
pixels: None,
|
||||
machine: Machine::new(),
|
||||
|
||||
frame_dt: Duration::from_nanos(1_000_000_000 / 60),
|
||||
next_frame: Instant::now(),
|
||||
|
||||
mouse_down: false,
|
||||
mouse_just_pressed: false,
|
||||
mouse_just_released: false,
|
||||
mouse_pos_fb: (0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
fn map_key(&mut self, code: KeyCode, is_down: bool) {
|
||||
match code {
|
||||
KeyCode::ArrowUp => self.machine.input.up = is_down,
|
||||
KeyCode::ArrowDown => self.machine.input.down = is_down,
|
||||
KeyCode::ArrowLeft => self.machine.input.left = is_down,
|
||||
KeyCode::ArrowRight => self.machine.input.right = is_down,
|
||||
|
||||
// A/B (troque depois como quiser)
|
||||
KeyCode::KeyZ => self.machine.input.a_down = is_down,
|
||||
KeyCode::KeyX => self.machine.input.b_down = is_down,
|
||||
|
||||
KeyCode::Enter => self.machine.input.start = is_down,
|
||||
KeyCode::ShiftLeft | KeyCode::ShiftRight => self.machine.input.select = is_down,
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_touch_in_core(&mut self) {
|
||||
// no desktop, mouse emula touch
|
||||
self.machine.touch.present = true;
|
||||
self.machine.touch.x = self.mouse_pos_fb.0;
|
||||
self.machine.touch.y = self.mouse_pos_fb.1;
|
||||
|
||||
// pressed/released devem durar 1 frame (o core limpa em begin_frame)
|
||||
self.machine.touch.pressed = self.mouse_just_pressed;
|
||||
self.machine.touch.released = self.mouse_just_released;
|
||||
self.machine.touch.down = self.mouse_down;
|
||||
|
||||
self.mouse_just_pressed = false;
|
||||
self.mouse_just_released = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for PrometeuApp {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let attrs = WindowAttributes::default()
|
||||
.with_title("PROMETEU - host_desktop")
|
||||
.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;
|
||||
self.map_key(code, is_down);
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
self.mouse_pos_fb = window_to_fb(position.x as f32, position.y as f32, self.window());
|
||||
}
|
||||
|
||||
WindowEvent::MouseInput { state, button, .. } => {
|
||||
if button == MouseButton::Left {
|
||||
match state {
|
||||
ElementState::Pressed => {
|
||||
if !self.mouse_down {
|
||||
self.mouse_down = true;
|
||||
self.mouse_just_pressed = true;
|
||||
}
|
||||
}
|
||||
ElementState::Released => {
|
||||
if self.mouse_down {
|
||||
self.mouse_down = false;
|
||||
self.mouse_just_released = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
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_dt;
|
||||
|
||||
// atualiza touch/input no core
|
||||
self.update_touch_in_core();
|
||||
|
||||
// executa 1 frame do PROMETEU
|
||||
self.machine.step_frame();
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user