docs: add Phase 3A Graphics Surface & Window Manager implementation plan
This commit is contained in:
parent
082421fe87
commit
ea9115fed8
1 changed files with 653 additions and 0 deletions
|
|
@ -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<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],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```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<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**
|
||||||
|
|
||||||
|
```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::<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**
|
||||||
|
|
||||||
|
```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::<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**
|
||||||
|
|
||||||
|
```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"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
Reference in a new issue