first window

This commit is contained in:
bQUARKz 2026-01-10 05:42:37 +00:00
parent ba673b4547
commit 4ccd38580f
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
9 changed files with 2789 additions and 34 deletions

2344
Cargo.lock generated

File diff suppressed because it is too large Load Diff

28
crates/core/src/color.rs Normal file
View 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
}
}

View File

@ -1,4 +1,6 @@
pub mod machine; pub mod machine;
pub mod peripherals; pub mod peripherals;
mod color;
pub use machine::Machine; pub use machine::Machine;
pub use color::Color;

View File

@ -1,7 +1,8 @@
use crate::peripherals::{Gfx, Input, Touch}; use crate::peripherals::{Gfx, Input, Touch};
use crate::Color;
/// PROMETEU "hardware lógico" (v0). /// 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 struct Machine {
pub gfx: Gfx, pub gfx: Gfx,
pub input: Input, pub input: Input,
@ -22,44 +23,47 @@ impl Machine {
} }
} }
/// Um frame lógico do PROMETEU. /// "Contrato de frame" do PROMETEU (Template Method sem loop).
/// (Depois isso chamará um "cartucho"; por enquanto é demo hardcoded.) /// O Host controla tempo/event loop; o Core define a sequência do frame.
pub fn tick(&mut self) { 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; 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)); 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 (x, y) = self.demo_pos();
let color = if self.input.a_down || self.touch.down { let color = if self.input.a_down || self.touch.down {
Color::rgb(0x00, 0xFF, 0x00) Color::rgb(0x00, 0xFF, 0x00)
} else { } else {
Color::rgb(0xFF, 0x40, 0x40) 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(); self.gfx.present();
} }
fn demo_pos(&self) -> (i32, i32) { fn demo_pos(&self) -> (i32, i32) {
// Só para mostrar movimento sem input por enquanto.
let t = (self.frame_index % 280) as i32; let t = (self.frame_index % 280) as i32;
(20 + t, 80) (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)
}
}

View File

@ -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 { pub struct Gfx {
w: usize, w: usize,
h: usize, h: usize,
front: Vec<u32>, front: Vec<u16>,
back: Vec<u32>, back: Vec<u16>,
} }
impl Gfx { impl Gfx {
@ -22,8 +37,8 @@ impl Gfx {
(self.w, self.h) (self.w, self.h)
} }
/// O buffer que o host deve exibir. /// O buffer que o host deve exibir (RGB565).
pub fn front_buffer(&self) -> &[u32] { pub fn front_buffer(&self) -> &[u16] {
&self.front &self.front
} }
@ -31,7 +46,16 @@ impl Gfx {
self.back.fill(color.0); 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 fw = self.w as i32;
let fh = self.h as i32; let fh = self.h as i32;
@ -40,16 +64,90 @@ impl Gfx {
let x1 = (x + w).clamp(0, fw); let x1 = (x + w).clamp(0, fw);
let y1 = (y + h).clamp(0, fh); let y1 = (y + h).clamp(0, fh);
let src = color.0;
for yy in y0..y1 { for yy in y0..y1 {
let row = (yy as usize) * self.w; let row = (yy as usize) * self.w;
for xx in x0..x1 { 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) { pub fn present(&mut self) {
std::mem::swap(&mut self.front, &mut self.back); 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)
}

View File

@ -11,3 +11,10 @@ pub struct Input {
pub start: bool, pub start: bool,
pub select: bool, pub select: bool,
} }
impl Input {
pub fn begin_frame(&mut self) {
// No v0, nada a fazer.
// No futuro: a_pressed/a_released, etc.
}
}

View File

@ -7,3 +7,11 @@ pub struct Touch {
pub x: i32, pub x: i32,
pub y: 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;
}
}

View File

@ -1,6 +1,9 @@
[package] [package]
name = "host_desktop" name = "host_desktop"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
[dependencies] [dependencies]
prometeu-core = { path = "../core" }
winit = "0.30.12"
pixels = "0.15.0"

View File

@ -1,3 +1,264 @@
fn main() { use std::time::{Duration, Instant};
println!("Hello, world!");
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)
} }