diff --git a/docs/superpowers/plans/2026-09-29-molotov-phase3a-graphics-surface.md b/docs/superpowers/plans/2026-09-29-molotov-phase3a-graphics-surface.md new file mode 100644 index 0000000..f1fcd61 --- /dev/null +++ b/docs/superpowers/plans/2026-09-29-molotov-phase3a-graphics-surface.md @@ -0,0 +1,653 @@ +# Molotov Phase 3A: Live SDL3 Window & Vulkan/OpenGL Graphics Surface Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Implement the Android `ANativeWindow` graphics surface backend and SDL3 windowing integration in `molotov-core`, allowing `libroblox.so` to acquire the native window handle, configure buffer geometries, lock/post presentation buffers, and bind Vulkan/OpenGL surfaces. + +**Architecture:** Provides the Android NDK `ANativeWindow` C-ABI (`ANativeWindow_*`) backed by an in-memory double-buffered presentation surface and SDL3 window handle dispatcher. Bridges window resizing, focus, and redraw events directly to `ANativeActivityCallbacks` (`onNativeWindowCreated`, `onNativeWindowResized`, `onNativeWindowRedrawNeeded`, `onNativeWindowDestroyed`). + +**Tech Stack:** Rust 1.75+, `libc`, `tracing`, `thiserror`. + +**Spec:** `docs/superpowers/specs/2026-09-29-molotov-architecture-design.md` (Sections 7.4 & 6.3) + +## Global Constraints + +- **Language & Edition**: Rust 1.75+ (2021 edition), strictly zero Wine or Proton. +- **License**: GNU General Public License v3.0 or later (GPL-3.0-or-later) with headers in all source files. +- **Android NDK Compatibility**: `ANativeWindow` struct layout must match Android NDK `native_window.h`. +- **Presentation Modes**: Supports Immediate, Mailbox, and FIFO frame pacing. +- **Memory Safety**: Double-buffered pixel surfaces with bounds-checked lock/unlock mechanics. + +## Review Focus + +1. **Window Buffer Geometry Resizing**: Dynamically altering buffer dimensions via `setBuffersGeometry` must reallocate pixel buffers without memory leaks or buffer under-allocation. +2. **Buffer Lock/Unlock Concurrency**: Calling `unlockAndPost` without an active `lock` (or double-locking) must return explicit error codes (`-EINVAL`), never corrupting frame buffers. +3. **Dirty Rectangle Clamping**: Dirty bounds passed to `lock` must be clamped within the window surface dimensions `[0..width, 0..height]`. +4. **Reference Counted Lifecycle**: `ANativeWindow_acquire` and `ANativeWindow_release` must maintain thread-safe reference counting; the surface is only deallocated when the final reference drops. +5. **Activity Window Lifecycle Synchronization**: Window resizing events from SDL3 must notify the engine via `onNativeWindowResized` with updated width and height. + +--- + +### Task 1: `ANativeWindow` C-ABI Structure & Format Definitions + +**Files:** +- Create: `crates/molotov-core/src/graphics/types.rs` +- Create: `crates/molotov-core/src/graphics/mod.rs` +- Test: `crates/molotov-core/tests/graphics_types_test.rs` + +**Interfaces:** +- Consumes: None +- Produces: + - `molotov_core::graphics::types::{ANativeWindowBuffer, ANativeWindow, WindowFormat}` + - Standard NDK constants (`WINDOW_FORMAT_RGBA_8888`, etc.) + +- [ ] **Step 1: Write the failing test for ANativeWindow struct layout and format definitions** + +```rust +// crates/molotov-core/tests/graphics_types_test.rs +use molotov_core::graphics::types::{WindowFormat, WINDOW_FORMAT_RGBA_8888, WINDOW_FORMAT_RGBX_8888}; + +#[test] +fn test_window_format_constants() { + assert_eq!(WindowFormat::Rgba8888 as i32, WINDOW_FORMAT_RGBA_8888); + assert_eq!(WindowFormat::Rgbx8888 as i32, WINDOW_FORMAT_RGBX_8888); + assert_eq!(WindowFormat::from_i32(1), Some(WindowFormat::Rgba8888)); + assert_eq!(WindowFormat::from_i32(999), None); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p molotov-core --test graphics_types_test` +Expected: FAIL with unexported module `graphics`. + +- [ ] **Step 3: Implement graphics types** + +```rust +// crates/molotov-core/src/graphics/types.rs +// SPDX-License-Identifier: GPL-3.0-or-later +use std::ffi::c_void; + +pub const WINDOW_FORMAT_RGBA_8888: i32 = 1; +pub const WINDOW_FORMAT_RGBX_8888: i32 = 2; +pub const WINDOW_FORMAT_RGB_565: i32 = 4; + +#[repr(i32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WindowFormat { + Rgba8888 = 1, + Rgbx8888 = 2, + Rgb565 = 4, +} + +impl WindowFormat { + pub fn from_i32(val: i32) -> Option { + match val { + 1 => Some(WindowFormat::Rgba8888), + 2 => Some(WindowFormat::Rgbx8888), + 4 => Some(WindowFormat::Rgb565), + _ => None, + } + } + + pub fn bytes_per_pixel(&self) -> usize { + match self { + WindowFormat::Rgba8888 | WindowFormat::Rgbx8888 => 4, + WindowFormat::Rgb565 => 2, + } + } +} + +#[repr(C)] +pub struct ARect { + pub left: i32, + pub top: i32, + pub right: i32, + pub bottom: i32, +} + +#[repr(C)] +pub struct ANativeWindow_Buffer { + pub width: i32, + pub height: i32, + pub stride: i32, + pub format: i32, + pub bits: *mut c_void, + pub reserved: [u32; 6], +} +``` + +```rust +// crates/molotov-core/src/graphics/mod.rs +// SPDX-License-Identifier: GPL-3.0-or-later +pub mod types; +``` + +Update `crates/molotov-core/src/lib.rs` to expose `pub mod graphics;`. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p molotov-core --test graphics_types_test` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/molotov-core/ +git commit -m "feat(graphics): implement ANativeWindow C-ABI types and pixel formats" +``` + +--- + +### Task 2: Double-Buffered Frame Queue & Surface Lock/Post Engine + +**Files:** +- Create: `crates/molotov-core/src/graphics/surface.rs` +- Modify: `crates/molotov-core/src/graphics/mod.rs` +- Test: `crates/molotov-core/tests/surface_buffer_test.rs` + +**Interfaces:** +- Consumes: `molotov_core::graphics::types::*` +- Produces: + - `molotov_core::graphics::surface::GraphicsSurface` + - `molotov_core::graphics::surface::GraphicsSurface::lock()` + - `molotov_core::graphics::surface::GraphicsSurface::unlock_and_post()` + +- [ ] **Step 1: Write the failing test for surface lock, buffer write, and post** + +```rust +// crates/molotov-core/tests/surface_buffer_test.rs +use molotov_core::graphics::surface::GraphicsSurface; +use molotov_core::graphics::types::WindowFormat; + +#[test] +fn test_surface_lock_and_unlock_post() { + let mut surface = GraphicsSurface::new(800, 600, WindowFormat::Rgba8888); + assert_eq!(surface.width(), 800); + assert_eq!(surface.height(), 600); + + let mut buffer = std::mem::MaybeUninit::uninit(); + let lock_res = unsafe { surface.lock(buffer.as_mut_ptr(), std::ptr::null_mut()) }; + assert_eq!(lock_res, 0); + + let buf = unsafe { buffer.assume_init() }; + assert_eq!(buf.width, 800); + assert_eq!(buf.height, 600); + assert!(!buf.bits.is_null()); + + let post_res = surface.unlock_and_post(); + assert_eq!(post_res, 0); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p molotov-core --test surface_buffer_test` +Expected: FAIL with unexported module `surface`. + +- [ ] **Step 3: Implement GraphicsSurface** + +```rust +// crates/molotov-core/src/graphics/surface.rs +// SPDX-License-Identifier: GPL-3.0-or-later +use super::types::{ANativeWindow_Buffer, ARect, WindowFormat}; +use std::ffi::c_void; +use std::sync::atomic::{AtomicU32, Ordering}; + +pub struct GraphicsSurface { + width: i32, + height: i32, + format: WindowFormat, + back_buffer: Vec, + front_buffer: Vec, + is_locked: bool, + ref_count: AtomicU32, +} + +impl GraphicsSurface { + pub fn new(width: i32, height: i32, format: WindowFormat) -> Self { + let size = (width as usize) * (height as usize) * format.bytes_per_pixel(); + Self { + width, + height, + format, + back_buffer: vec![0u8; size], + front_buffer: vec![0u8; size], + is_locked: false, + ref_count: AtomicU32::new(1), + } + } + + pub fn width(&self) -> i32 { + self.width + } + + pub fn height(&self) -> i32 { + self.height + } + + pub fn format(&self) -> WindowFormat { + self.format + } + + pub fn acquire(&self) -> u32 { + self.ref_count.fetch_add(1, Ordering::Relaxed) + 1 + } + + pub fn release(&self) -> u32 { + self.ref_count.fetch_sub(1, Ordering::Release) - 1 + } + + pub fn set_geometry(&mut self, width: i32, height: i32, format: WindowFormat) { + if width > 0 && height > 0 { + self.width = width; + self.height = height; + self.format = format; + let size = (width as usize) * (height as usize) * format.bytes_per_pixel(); + self.back_buffer.resize(size, 0); + self.front_buffer.resize(size, 0); + } + } + + pub unsafe fn lock( + &mut self, + out_buffer: *mut ANativeWindow_Buffer, + _in_out_dirty: *mut ARect, + ) -> i32 { + if out_buffer.is_null() { + return -libc::EINVAL; + } + if self.is_locked { + return -libc::EBUSY; + } + + self.is_locked = true; + + (*out_buffer).width = self.width; + (*out_buffer).height = self.height; + (*out_buffer).stride = self.width; + (*out_buffer).format = self.format as i32; + (*out_buffer).bits = self.back_buffer.as_mut_ptr() as *mut c_void; + (*out_buffer).reserved = [0; 6]; + + 0 + } + + pub fn unlock_and_post(&mut self) -> i32 { + if !self.is_locked { + return -libc::EINVAL; + } + + // Swap front and back buffer for presentation + std::mem::swap(&mut self.front_buffer, &mut self.back_buffer); + self.is_locked = false; + 0 + } +} +``` + +Update `crates/molotov-core/src/graphics/mod.rs` to expose `pub mod surface;`. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p molotov-core --test surface_buffer_test` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/molotov-core/ +git commit -m "feat(graphics): implement double-buffered graphics surface with lock and post engine" +``` + +--- + +### Task 3: SDL3 Window Manager & NativeActivity Surface Bridge + +**Files:** +- Create: `crates/molotov-core/src/graphics/window.rs` +- Modify: `crates/molotov-core/src/graphics/mod.rs` +- Test: `crates/molotov-core/tests/window_manager_test.rs` + +**Interfaces:** +- Consumes: `molotov_core::graphics::surface::GraphicsSurface` +- Produces: + - `molotov_core::graphics::window::WindowManager` + - `molotov_core::graphics::window::WindowConfig` + +- [ ] **Step 1: Write the failing test for window manager configuration and event bridge** + +```rust +// crates/molotov-core/tests/window_manager_test.rs +use molotov_core::graphics::window::{WindowConfig, WindowManager}; + +#[test] +fn test_window_manager_configuration_and_resize() { + let config = WindowConfig { + title: "Roblox".to_string(), + width: 1280, + height: 720, + fullscreen: false, + vulkan: true, + }; + + let mut manager = WindowManager::new(config); + assert_eq!(manager.width(), 1280); + assert_eq!(manager.height(), 720); + + // Simulate display resize + manager.handle_resize(1920, 1080); + assert_eq!(manager.width(), 1920); + assert_eq!(manager.height(), 1080); + assert_eq!(manager.surface.width(), 1920); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p molotov-core --test window_manager_test` +Expected: FAIL with unexported module `window`. + +- [ ] **Step 3: Implement WindowManager** + +```rust +// crates/molotov-core/src/graphics/window.rs +// SPDX-License-Identifier: GPL-3.0-or-later +use super::surface::GraphicsSurface; +use super::types::WindowFormat; + +#[derive(Debug, Clone)] +pub struct WindowConfig { + pub title: String, + pub width: i32, + pub height: i32, + pub fullscreen: bool, + pub vulkan: bool, +} + +impl Default for WindowConfig { + fn default() -> Self { + Self { + title: "Molotov - Roblox".to_string(), + width: 1280, + height: 720, + fullscreen: false, + vulkan: true, + } + } +} + +pub struct WindowManager { + config: WindowConfig, + pub surface: GraphicsSurface, +} + +impl WindowManager { + pub fn new(config: WindowConfig) -> Self { + let surface = GraphicsSurface::new(config.width, config.height, WindowFormat::Rgba8888); + Self { config, surface } + } + + pub fn width(&self) -> i32 { + self.surface.width() + } + + pub fn height(&self) -> i32 { + self.surface.height() + } + + pub fn handle_resize(&mut self, width: i32, height: i32) { + if width > 0 && height > 0 { + self.config.width = width; + self.config.height = height; + self.surface.set_geometry(width, height, self.surface.format()); + tracing::info!("WindowManager: surface resized to {}x{}", width, height); + } + } +} +``` + +Update `crates/molotov-core/src/graphics/mod.rs` to expose `pub mod window;`. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p molotov-core --test window_manager_test` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/molotov-core/ +git commit -m "feat(graphics): implement window manager and display resize handler" +``` + +--- + +### Task 4: Exported `ANativeWindow_*` NDK C-ABI Functions + +**Files:** +- Create: `crates/molotov-core/src/graphics/c_api.rs` +- Modify: `crates/molotov-core/src/graphics/mod.rs` +- Test: `crates/molotov-core/tests/anativewindow_c_api_test.rs` + +**Interfaces:** +- Consumes: `molotov_core::graphics::surface::GraphicsSurface` +- Produces: + - `ANativeWindow_acquire` + - `ANativeWindow_release` + - `ANativeWindow_getWidth` + - `ANativeWindow_getHeight` + - `ANativeWindow_getFormat` + - `ANativeWindow_setBuffersGeometry` + - `ANativeWindow_lock` + - `ANativeWindow_unlockAndPost` + +- [ ] **Step 1: Write the failing test for exported ANativeWindow C-ABI functions** + +```rust +// crates/molotov-core/tests/anativewindow_c_api_test.rs +use molotov_core::graphics::c_api::*; +use molotov_core::graphics::surface::GraphicsSurface; +use molotov_core::graphics::types::{ANativeWindow_Buffer, WindowFormat}; + +#[test] +fn test_anativewindow_c_api_lifecycle() { + let surface = Box::new(GraphicsSurface::new(640, 480, WindowFormat::Rgba8888)); + let handle = Box::into_raw(surface) as *mut (); + + unsafe { + assert_eq!(ANativeWindow_getWidth(handle), 640); + assert_eq!(ANativeWindow_getHeight(handle), 480); + assert_eq!(ANativeWindow_getFormat(handle), 1); + + ANativeWindow_setBuffersGeometry(handle, 800, 600, 1); + assert_eq!(ANativeWindow_getWidth(handle), 800); + assert_eq!(ANativeWindow_getHeight(handle), 600); + + let mut buf = std::mem::MaybeUninit::::uninit(); + assert_eq!(ANativeWindow_lock(handle, buf.as_mut_ptr(), std::ptr::null_mut()), 0); + assert_eq!(ANativeWindow_unlockAndPost(handle), 0); + + ANativeWindow_release(handle); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p molotov-core --test anativewindow_c_api_test` +Expected: FAIL with unexported module `c_api`. + +- [ ] **Step 3: Implement exported C-ABI functions** + +```rust +// crates/molotov-core/src/graphics/c_api.rs +// SPDX-License-Identifier: GPL-3.0-or-later +#![allow(non_snake_case)] +use super::surface::GraphicsSurface; +use super::types::{ANativeWindow_Buffer, ARect, WindowFormat}; + +#[no_mangle] +pub unsafe extern "C" fn ANativeWindow_acquire(window: *mut ()) { + if let Some(s) = (window as *mut GraphicsSurface).as_ref() { + s.acquire(); + } +} + +#[no_mangle] +pub unsafe extern "C" fn ANativeWindow_release(window: *mut ()) { + if !window.is_null() { + let ptr = window as *mut GraphicsSurface; + if (*ptr).release() == 0 { + drop(Box::from_raw(ptr)); + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn ANativeWindow_getWidth(window: *mut ()) -> i32 { + if let Some(s) = (window as *mut GraphicsSurface).as_ref() { + s.width() + } else { + 0 + } +} + +#[no_mangle] +pub unsafe extern "C" fn ANativeWindow_getHeight(window: *mut ()) -> i32 { + if let Some(s) = (window as *mut GraphicsSurface).as_ref() { + s.height() + } else { + 0 + } +} + +#[no_mangle] +pub unsafe extern "C" fn ANativeWindow_getFormat(window: *mut ()) -> i32 { + if let Some(s) = (window as *mut GraphicsSurface).as_ref() { + s.format() as i32 + } else { + 0 + } +} + +#[no_mangle] +pub unsafe extern "C" fn ANativeWindow_setBuffersGeometry( + window: *mut (), + width: i32, + height: i32, + format: i32, +) -> i32 { + if let Some(s) = (window as *mut GraphicsSurface).as_mut() { + let fmt = WindowFormat::from_i32(format).unwrap_or(WindowFormat::Rgba8888); + s.set_geometry(width, height, fmt); + 0 + } else { + -libc::EINVAL + } +} + +#[no_mangle] +pub unsafe extern "C" fn ANativeWindow_lock( + window: *mut (), + outBuffer: *mut ANativeWindow_Buffer, + inOutDirtyBounds: *mut ARect, +) -> i32 { + if let Some(s) = (window as *mut GraphicsSurface).as_mut() { + s.lock(outBuffer, inOutDirtyBounds) + } else { + -libc::EINVAL + } +} + +#[no_mangle] +pub unsafe extern "C" fn ANativeWindow_unlockAndPost(window: *mut ()) -> i32 { + if let Some(s) = (window as *mut GraphicsSurface).as_mut() { + s.unlock_and_post() + } else { + -libc::EINVAL + } +} +``` + +Update `crates/molotov-core/src/graphics/mod.rs` to expose `pub mod c_api;`. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p molotov-core --test anativewindow_c_api_test` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/molotov-core/ +git commit -m "feat(graphics): implement ANativeWindow C-ABI exported functions" +``` + +--- + +### Task 5: End-to-End Graphics Surface & Window Event Integration Test + +**Files:** +- Create: `crates/molotov-core/tests/graphics_surface_test.rs` +- Modify: `crates/molotov-core/src/graphics/mod.rs` + +**Interfaces:** +- Consumes: All `molotov_core::graphics::*` modules, `molotov_core::jni::activity::ActivityRunner` +- Produces: Verified end-to-end window creation, surface binding, and rendering cycle + +- [ ] **Step 1: Write integration test binding GraphicsSurface to ANativeActivity** + +```rust +// crates/molotov-core/tests/graphics_surface_test.rs +use molotov_core::graphics::c_api::*; +use molotov_core::graphics::surface::GraphicsSurface; +use molotov_core::graphics::types::{ANativeWindow_Buffer, WindowFormat}; +use molotov_core::jni::activity::ActivityRunner; + +#[test] +fn test_end_to_end_graphics_surface_and_activity_binding() { + let mut runner = ActivityRunner::new(); + let surface = Box::new(GraphicsSurface::new(1280, 720, WindowFormat::Rgba8888)); + let window_handle = Box::into_raw(surface) as *mut (); + + unsafe { + // 1. Notify activity that native window was created + runner.trigger_window_created(window_handle as *mut _); + + // 2. Engine queries window geometry + assert_eq!(ANativeWindow_getWidth(window_handle), 1280); + assert_eq!(ANativeWindow_getHeight(window_handle), 720); + + // 3. Engine renders frame + let mut buf = std::mem::MaybeUninit::::uninit(); + assert_eq!(ANativeWindow_lock(window_handle, buf.as_mut_ptr(), std::ptr::null_mut()), 0); + + let buffer = buf.assume_init(); + // Write solid color into buffer (RGBA = 255, 0, 0, 255) + let pixel_count = (buffer.width * buffer.height) as usize; + let pixels = std::slice::from_raw_parts_mut(buffer.bits as *mut u32, pixel_count); + pixels.fill(0xff00_00ff); + + assert_eq!(ANativeWindow_unlockAndPost(window_handle), 0); + + // 4. Notify activity on window destroy + runner.trigger_window_destroyed(window_handle as *mut _); + ANativeWindow_release(window_handle); + } +} +``` + +- [ ] **Step 2: Run test to verify it passes** + +Run: `cargo test -p molotov-core --test graphics_surface_test` +Expected: PASS. + +- [ ] **Step 3: Commit** + +```bash +git add crates/molotov-core/tests/graphics_surface_test.rs +git commit -m "test(graphics): implement end-to-end graphics surface and activity window integration test" +``` + +---