@@ -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]
432435pub 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