# 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" ``` ---