Skip to content

Commit f3a7f9f

Browse files
committed
Add nearest-neighbor texture filtering + fix StringHeader for Perry 0.4.x
- renderer.rs: Add nearest_sampler alongside linear sampler, add set_texture_filter() method to switch per-texture - string_header.rs: Update StringHeader to 12 bytes (add refcount field) matching Perry 0.4.x runtime format - All platforms: Add bloom_set_texture_filter FFI function, update bloom_read_file to return 12-byte Perry string headers - textures/index.ts: Add setTextureFilter(), FILTER_LINEAR, FILTER_NEAREST - package.json: Add bloom_set_texture_filter FFI declaration
1 parent 2782c58 commit f3a7f9f

9 files changed

Lines changed: 426 additions & 37 deletions

File tree

native/android/src/lib.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -857,19 +857,18 @@ pub extern "C" fn bloom_read_file(path_ptr: *const u8) -> *const u8 {
857857
let path = str_from_header(path_ptr);
858858
match std::fs::read_to_string(resolve_path(path)) {
859859
Ok(contents) => {
860-
// Return Perry-format string: StringHeader (length u32 + capacity u32) followed by UTF-8 data
860+
// Return Perry-format string: StringHeader (length u32 + capacity u32 + refcount u32) followed by UTF-8 data
861861
let bytes = contents.as_bytes();
862862
let len = bytes.len();
863-
let total = 8 + len; // 8 bytes header + data
863+
let total = 12 + len; // 12 bytes header (3 × u32) + data
864864
let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
865865
unsafe {
866866
let ptr = std::alloc::alloc(layout);
867867
if ptr.is_null() { return std::ptr::null(); }
868-
// Write length and capacity as u32
869-
*(ptr as *mut u32) = len as u32;
870-
*(ptr.add(4) as *mut u32) = len as u32;
871-
// Copy string data after header
872-
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(8), len);
868+
*(ptr as *mut u32) = len as u32; // length
869+
*(ptr.add(4) as *mut u32) = len as u32; // capacity
870+
*(ptr.add(8) as *mut u32) = 1; // refcount (unique)
871+
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
873872
ptr
874873
}
875874
}

native/ios/src/lib.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,19 +1422,18 @@ pub extern "C" fn bloom_read_file(path_ptr: *const u8) -> *const u8 {
14221422
let path = str_from_header(path_ptr);
14231423
match std::fs::read_to_string(resolve_path(path)) {
14241424
Ok(contents) => {
1425-
// Return Perry-format string: StringHeader (length u32 + capacity u32) followed by UTF-8 data
1425+
// Return Perry-format string: StringHeader (length u32 + capacity u32 + refcount u32) followed by UTF-8 data
14261426
let bytes = contents.as_bytes();
14271427
let len = bytes.len();
1428-
let total = 8 + len; // 8 bytes header + data
1428+
let total = 12 + len; // 12 bytes header (3 × u32) + data
14291429
let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
14301430
unsafe {
14311431
let ptr = std::alloc::alloc(layout);
14321432
if ptr.is_null() { return std::ptr::null(); }
1433-
// Write length and capacity as u32
1434-
*(ptr as *mut u32) = len as u32;
1435-
*(ptr.add(4) as *mut u32) = len as u32;
1436-
// Copy string data after header
1437-
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(8), len);
1433+
*(ptr as *mut u32) = len as u32; // length
1434+
*(ptr.add(4) as *mut u32) = len as u32; // capacity
1435+
*(ptr.add(8) as *mut u32) = 1; // refcount (unique)
1436+
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
14381437
ptr
14391438
}
14401439
}

native/linux/src/lib.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -904,19 +904,18 @@ pub extern "C" fn bloom_read_file(path_ptr: *const u8) -> *const u8 {
904904
let path = str_from_header(path_ptr);
905905
match std::fs::read_to_string(path) {
906906
Ok(contents) => {
907-
// Return Perry-format string: StringHeader (length u32 + capacity u32) followed by UTF-8 data
907+
// Return Perry-format string: StringHeader (length u32 + capacity u32 + refcount u32) followed by UTF-8 data
908908
let bytes = contents.as_bytes();
909909
let len = bytes.len();
910-
let total = 8 + len; // 8 bytes header + data
910+
let total = 12 + len; // 12 bytes header (3 × u32) + data
911911
let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
912912
unsafe {
913913
let ptr = std::alloc::alloc(layout);
914914
if ptr.is_null() { return std::ptr::null(); }
915-
// Write length and capacity as u32
916-
*(ptr as *mut u32) = len as u32;
917-
*(ptr.add(4) as *mut u32) = len as u32;
918-
// Copy string data after header
919-
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(8), len);
915+
*(ptr as *mut u32) = len as u32; // length
916+
*(ptr.add(4) as *mut u32) = len as u32; // capacity
917+
*(ptr.add(8) as *mut u32) = 1; // refcount (unique)
918+
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
920919
ptr
921920
}
922921
}

native/macos/src/lib.rs

Lines changed: 255 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u
310310
.unwrap_or(surface_caps.formats[0]);
311311

312312
let surface_config = wgpu::SurfaceConfiguration {
313-
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
313+
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
314314
format,
315315
width: width as u32,
316316
height: height as u32,
@@ -328,6 +328,9 @@ pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u
328328
let _ = ENGINE.set(engine_state);
329329
WINDOW = Some(window);
330330
}
331+
332+
// Register Bloom's GPU screenshot capture with perry-geisterhand (if linked)
333+
bloom_register_geisterhand_screenshot();
331334
}
332335

333336
#[no_mangle]
@@ -430,6 +433,11 @@ pub extern "C" fn bloom_begin_drawing() {
430433

431434
#[no_mangle]
432435
pub extern "C" fn bloom_end_drawing() {
436+
// Pump geisterhand BEFORE end_frame.
437+
// Screenshot function re-renders inline with captured VP + vertices.
438+
extern "C" { fn perry_geisterhand_pump(); }
439+
unsafe { perry_geisterhand_pump(); }
440+
433441
engine().end_frame();
434442
}
435443

@@ -1295,19 +1303,18 @@ pub extern "C" fn bloom_read_file(path_ptr: *const u8) -> *const u8 {
12951303
let path = str_from_header(path_ptr);
12961304
match std::fs::read_to_string(path) {
12971305
Ok(contents) => {
1298-
// Return Perry-format string: StringHeader (length u32 + capacity u32) followed by UTF-8 data
1306+
// Return Perry-format string: StringHeader (length u32 + capacity u32 + refcount u32) followed by UTF-8 data
12991307
let bytes = contents.as_bytes();
13001308
let len = bytes.len();
1301-
let total = 8 + len; // 8 bytes header + data
1309+
let total = 12 + len; // 12 bytes header (3 × u32) + data
13021310
let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
13031311
unsafe {
13041312
let ptr = std::alloc::alloc(layout);
13051313
if ptr.is_null() { return std::ptr::null(); }
1306-
// Write length and capacity as u32
1307-
*(ptr as *mut u32) = len as u32;
1308-
*(ptr.add(4) as *mut u32) = len as u32;
1309-
// Copy string data after header
1310-
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(8), len);
1314+
*(ptr as *mut u32) = len as u32; // length
1315+
*(ptr.add(4) as *mut u32) = len as u32; // capacity
1316+
*(ptr.add(8) as *mut u32) = 1; // refcount (unique)
1317+
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
13111318
ptr
13121319
}
13131320
}
@@ -1493,6 +1500,24 @@ pub extern "C" fn bloom_scene_node_count() -> f64 {
14931500
engine().scene.node_count() as f64
14941501
}
14951502

1503+
/// Debug: get vertex count for a scene node (0 if not found or empty)
1504+
#[no_mangle]
1505+
pub extern "C" fn bloom_scene_node_vertex_count(handle: f64) -> f64 {
1506+
match engine().scene.nodes.get(handle) {
1507+
Some(node) => node.vertices.len() as f64,
1508+
None => -1.0,
1509+
}
1510+
}
1511+
1512+
/// Debug: get index count for a scene node
1513+
#[no_mangle]
1514+
pub extern "C" fn bloom_scene_node_index_count(handle: f64) -> f64 {
1515+
match engine().scene.nodes.get(handle) {
1516+
Some(node) => node.indices.len() as f64,
1517+
None => -1.0,
1518+
}
1519+
}
1520+
14961521
// ============================================================
14971522
// Geometry generation
14981523
// ============================================================
@@ -1605,6 +1630,49 @@ pub extern "C" fn bloom_postfx_set_outline_thickness(thickness: f64) {
16051630
}
16061631
}
16071632

1633+
// ============================================================
1634+
// 3D→2D Projection (for UI overlays positioned in 3D space)
1635+
// ============================================================
1636+
1637+
/// Project a world-space 3D point to screen coordinates.
1638+
/// Returns screen X. Call bloom_project_y for Y. Returns -9999 if behind camera.
1639+
static mut LAST_PROJECT: (f64, f64) = (0.0, 0.0);
1640+
1641+
#[no_mangle]
1642+
pub extern "C" fn bloom_project_to_screen(wx: f64, wy: f64, wz: f64) -> f64 {
1643+
let eng = engine();
1644+
let vp = eng.renderer.vp_matrix();
1645+
let w = eng.renderer.width() as f32;
1646+
let h = eng.renderer.height() as f32;
1647+
1648+
// Multiply by VP matrix
1649+
let x = wx as f32;
1650+
let y = wy as f32;
1651+
let z = wz as f32;
1652+
let clip_x = vp[0][0]*x + vp[1][0]*y + vp[2][0]*z + vp[3][0];
1653+
let clip_y = vp[0][1]*x + vp[1][1]*y + vp[2][1]*z + vp[3][1];
1654+
let clip_w = vp[0][3]*x + vp[1][3]*y + vp[2][3]*z + vp[3][3];
1655+
1656+
if clip_w <= 0.0 {
1657+
unsafe { LAST_PROJECT = (-9999.0, -9999.0); }
1658+
return -9999.0;
1659+
}
1660+
1661+
// NDC to screen
1662+
let ndc_x = clip_x / clip_w;
1663+
let ndc_y = clip_y / clip_w;
1664+
let screen_x = ((ndc_x + 1.0) * 0.5 * w) as f64;
1665+
let screen_y = ((1.0 - ndc_y) * 0.5 * h) as f64;
1666+
1667+
unsafe { LAST_PROJECT = (screen_x, screen_y); }
1668+
screen_x
1669+
}
1670+
1671+
#[no_mangle]
1672+
pub extern "C" fn bloom_project_screen_y() -> f64 {
1673+
unsafe { LAST_PROJECT.1 }
1674+
}
1675+
16081676
/// Attach a loaded GLTF model's mesh geometry to a scene node.
16091677
/// Copies the vertex/index data from the model into the scene node.
16101678
#[no_mangle]
@@ -1761,6 +1829,185 @@ fn pollster_block_on<F: std::future::Future>(future: F) -> F::Output {
17611829
}
17621830
}
17631831

1832+
// ============================================================
1833+
// Geisterhand screenshot integration
1834+
// ============================================================
1835+
1836+
/// Register Bloom's GPU-based screenshot capture with perry-geisterhand.
1837+
/// This replaces perry-ui-macos's CGWindowListCreateImage approach with
1838+
/// direct wgpu texture readback — works for Metal/Vulkan rendered content.
1839+
fn bloom_register_geisterhand_screenshot() {
1840+
// Try to register with geisterhand if it's linked (weak symbol)
1841+
extern "C" {
1842+
fn perry_geisterhand_register_screenshot_capture(
1843+
f: extern "C" fn(*mut usize) -> *mut u8,
1844+
);
1845+
}
1846+
unsafe {
1847+
perry_geisterhand_register_screenshot_capture(bloom_screenshot_capture);
1848+
}
1849+
}
1850+
1851+
/// Capture the Bloom framebuffer as PNG.
1852+
/// Called from geisterhand pump BEFORE end_frame in bloom_end_drawing.
1853+
/// The vertices_3d/2d and VP matrix from the game loop are still populated.
1854+
/// We render to a fresh surface texture with screenshot capture, producing
1855+
/// the same visual output as the real frame.
1856+
extern "C" fn bloom_screenshot_capture(out_len: *mut usize) -> *mut u8 {
1857+
let eng = engine();
1858+
1859+
// Set capture flag and render inline
1860+
eng.renderer.screenshot_requested = true;
1861+
eng.scene.prepare(
1862+
&eng.renderer.device,
1863+
&eng.renderer.queue,
1864+
&eng.renderer.vp_matrix(),
1865+
eng.renderer.uniform_3d_layout(),
1866+
);
1867+
eng.renderer.end_frame_with_scene(&eng.scene);
1868+
1869+
match eng.renderer.screenshot_data.take() {
1870+
Some((width, height, rgba)) => {
1871+
// Encode RGBA pixels to PNG
1872+
match encode_png(width, height, &rgba) {
1873+
Some(png_data) => {
1874+
let len = png_data.len();
1875+
// Allocate with libc::malloc (caller will free with libc::free)
1876+
let ptr = unsafe { libc::malloc(len) as *mut u8 };
1877+
if ptr.is_null() {
1878+
unsafe { *out_len = 0; }
1879+
return std::ptr::null_mut();
1880+
}
1881+
unsafe {
1882+
std::ptr::copy_nonoverlapping(png_data.as_ptr(), ptr, len);
1883+
*out_len = len;
1884+
}
1885+
ptr
1886+
}
1887+
None => {
1888+
unsafe { *out_len = 0; }
1889+
std::ptr::null_mut()
1890+
}
1891+
}
1892+
}
1893+
None => {
1894+
unsafe { *out_len = 0; }
1895+
std::ptr::null_mut()
1896+
}
1897+
}
1898+
}
1899+
1900+
/// Minimal PNG encoder (no external dependency).
1901+
fn encode_png(width: u32, height: u32, rgba: &[u8]) -> Option<Vec<u8>> {
1902+
use std::io::Write;
1903+
1904+
let mut png = Vec::new();
1905+
// PNG signature
1906+
png.write_all(&[137, 80, 78, 71, 13, 10, 26, 10]).ok()?;
1907+
1908+
// IHDR chunk
1909+
let mut ihdr = Vec::new();
1910+
ihdr.extend_from_slice(&width.to_be_bytes());
1911+
ihdr.extend_from_slice(&height.to_be_bytes());
1912+
ihdr.push(8); // bit depth
1913+
ihdr.push(6); // color type: RGBA
1914+
ihdr.push(0); // compression
1915+
ihdr.push(0); // filter
1916+
ihdr.push(0); // interlace
1917+
write_png_chunk(&mut png, b"IHDR", &ihdr);
1918+
1919+
// IDAT chunk — raw pixel data with zlib
1920+
// Build raw scanlines: each row starts with filter byte 0 (None)
1921+
let row_bytes = (width * 4) as usize;
1922+
let mut raw = Vec::with_capacity((row_bytes + 1) * height as usize);
1923+
for y in 0..height as usize {
1924+
raw.push(0); // filter: None
1925+
let start = y * row_bytes;
1926+
// Copy BGRA pixels, swapping B and R for PNG (which expects RGBA)
1927+
for x in 0..width as usize {
1928+
let idx = start + x * 4;
1929+
// Metal Bgra8UnormSrgb: byte order is B, G, R, A
1930+
raw.push(rgba[idx + 2]); // R (was at offset 2 in BGRA)
1931+
raw.push(rgba[idx + 1]); // G (same position)
1932+
raw.push(rgba[idx + 0]); // B (was at offset 0 in BGRA)
1933+
raw.push(255); // A (force opaque — alpha from sRGB surface is unreliable)
1934+
}
1935+
}
1936+
1937+
// Compress with deflate (store blocks, no actual compression for simplicity)
1938+
let deflated = deflate_store(&raw);
1939+
write_png_chunk(&mut png, b"IDAT", &deflated);
1940+
1941+
// IEND chunk
1942+
write_png_chunk(&mut png, b"IEND", &[]);
1943+
1944+
Some(png)
1945+
}
1946+
1947+
fn write_png_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) {
1948+
let len = data.len() as u32;
1949+
out.extend_from_slice(&len.to_be_bytes());
1950+
out.extend_from_slice(chunk_type);
1951+
out.extend_from_slice(data);
1952+
// CRC32 over type + data
1953+
let crc = crc32(&[chunk_type.as_slice(), data].concat());
1954+
out.extend_from_slice(&crc.to_be_bytes());
1955+
}
1956+
1957+
fn crc32(data: &[u8]) -> u32 {
1958+
let mut crc: u32 = 0xFFFFFFFF;
1959+
for &byte in data {
1960+
crc ^= byte as u32;
1961+
for _ in 0..8 {
1962+
if crc & 1 != 0 {
1963+
crc = (crc >> 1) ^ 0xEDB88320;
1964+
} else {
1965+
crc >>= 1;
1966+
}
1967+
}
1968+
}
1969+
!crc
1970+
}
1971+
1972+
/// Minimal deflate: store blocks (no compression). Wraps in zlib format.
1973+
fn deflate_store(data: &[u8]) -> Vec<u8> {
1974+
let mut out = Vec::new();
1975+
// Zlib header: CMF=0x78 (deflate, window=32K), FLG=0x01 (no dict, check bits)
1976+
out.push(0x78);
1977+
out.push(0x01);
1978+
1979+
// Split into 65535-byte store blocks
1980+
let mut remaining = data.len();
1981+
let mut offset = 0;
1982+
while remaining > 0 {
1983+
let block_size = remaining.min(65535);
1984+
let is_last = remaining <= 65535;
1985+
out.push(if is_last { 1 } else { 0 }); // BFINAL + BTYPE=00 (store)
1986+
let len = block_size as u16;
1987+
let nlen = !len;
1988+
out.extend_from_slice(&len.to_le_bytes());
1989+
out.extend_from_slice(&nlen.to_le_bytes());
1990+
out.extend_from_slice(&data[offset..offset + block_size]);
1991+
offset += block_size;
1992+
remaining -= block_size;
1993+
}
1994+
1995+
// Adler-32 checksum
1996+
let adler = adler32(data);
1997+
out.extend_from_slice(&adler.to_be_bytes());
1998+
out
1999+
}
2000+
2001+
fn adler32(data: &[u8]) -> u32 {
2002+
let mut a: u32 = 1;
2003+
let mut b: u32 = 0;
2004+
for &byte in data {
2005+
a = (a + byte as u32) % 65521;
2006+
b = (b + a) % 65521;
2007+
}
2008+
(b << 16) | a
2009+
}
2010+
17642011
// ============================================================
17652012
// Scene picking (raycasting)
17662013
// ============================================================

0 commit comments

Comments
 (0)