This repository has been archived on 2026-10-10. You can view files and clone it, but cannot push or open issues or pull requests.
molotov/docs/superpowers/plans/2026-09-29-molotov-phase3a-graphics-surface.md

20 KiB

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

// 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
// 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<Self> {
        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],
}
// 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
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

// 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
// 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<u8>,
    front_buffer: Vec<u8>,
    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
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

// 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
// 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
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

// 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::<ANativeWindow_Buffer>::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
// 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
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

// 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::<ANativeWindow_Buffer>::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
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"