pub use syphon_core::{SyphonServer, SyphonClient, SyphonError, Result, ServerInfo, ServerOptions};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PublishStatus {
ZeroCopy,
CpuFallback,
NoClients,
PoolExhausted,
}
#[derive(Debug, Clone)]
pub struct SyphonOutputConfig {
pub pool_size: usize,
pub server_options: ServerOptions,
}
impl Default for SyphonOutputConfig {
fn default() -> Self {
Self { pool_size: 3, server_options: ServerOptions::default() }
}
}
pub mod input;
pub use input::SyphonWgpuInput;
#[cfg(target_os = "macos")]
use syphon_metal::wgpu_interop;
#[cfg(target_os = "macos")]
use objc2::runtime::{AnyObject, ProtocolObject};
#[cfg(target_os = "macos")]
use objc2_metal::{
MTLCommandBuffer, MTLCommandQueue, MTLDevice, MTLOrigin, MTLPixelFormat, MTLRegion,
MTLSize, MTLStorageMode, MTLTexture, MTLTextureDescriptor, MTLTextureUsage,
};
pub struct SyphonWgpuOutput {
server: SyphonServer,
width: u32,
height: u32,
#[cfg(target_os = "macos")]
surface_pool: syphon_metal::IOSurfacePool,
#[cfg(target_os = "macos")]
frame_count: u64,
#[cfg(target_os = "macos")]
use_zero_copy: bool,
#[cfg(target_os = "macos")]
metal_ctx: syphon_metal::MetalContext,
}
#[cfg(target_os = "macos")]
unsafe impl Send for SyphonWgpuOutput {}
#[cfg(target_os = "macos")]
unsafe impl Sync for SyphonWgpuOutput {}
impl SyphonWgpuOutput {
pub fn new(
name: &str,
wgpu_device: &wgpu::Device,
wgpu_queue: &wgpu::Queue,
width: u32,
height: u32,
) -> Result<Self> {
Self::new_with_config(name, wgpu_device, wgpu_queue, width, height, SyphonOutputConfig::default())
}
pub fn new_with_config(
name: &str,
wgpu_device: &wgpu::Device,
_wgpu_queue: &wgpu::Queue,
width: u32,
height: u32,
config: SyphonOutputConfig,
) -> Result<Self> {
#[cfg(target_os = "macos")]
{
Self::new_macos(name, wgpu_device, _wgpu_queue, width, height, config)
}
#[cfg(not(target_os = "macos"))]
{
let _ = (name, wgpu_device, _wgpu_queue, width, height, config);
Err(SyphonError::NotAvailable)
}
}
#[cfg(target_os = "macos")]
fn new_macos(
name: &str,
wgpu_device: &wgpu::Device,
_wgpu_queue: &wgpu::Queue,
width: u32,
height: u32,
config: SyphonOutputConfig,
) -> Result<Self> {
let ctx_opt = syphon_metal::MetalContext::from_wgpu_device(wgpu_device);
let use_zero_copy = ctx_opt.is_some();
let metal_ctx = match ctx_opt {
Some(ctx) => {
log::info!("SyphonWgpuOutput: Using zero-copy GPU-to-GPU path");
ctx
}
None => {
log::warn!("SyphonWgpuOutput: Metal interop failed, falling back to CPU readback");
syphon_metal::MetalContext::system_default()
.ok_or_else(|| SyphonError::CreateFailed(
"Failed to get Metal device".to_string()
))?
}
};
let device_ptr =
metal_ctx.device() as *const ProtocolObject<dyn MTLDevice> as *mut AnyObject;
let server = SyphonServer::new_with_name_and_device_and_options(
name, device_ptr, width, height, config.server_options.clone()
)?;
let pool_size = if use_zero_copy { config.pool_size.max(1) } else { 0 };
let surface_pool = syphon_metal::IOSurfacePool::new(width, height, pool_size);
log::info!(
"SyphonWgpuOutput created: {}x{} ({})",
width, height,
if use_zero_copy { "zero-copy" } else { "CPU fallback" }
);
Ok(Self {
server,
width,
height,
surface_pool,
frame_count: 0,
use_zero_copy,
metal_ctx,
})
}
pub fn publish(
&mut self,
texture: &wgpu::Texture,
device: &wgpu::Device,
queue: &wgpu::Queue,
) -> PublishStatus {
#[cfg(target_os = "macos")]
{
if self.server.client_count() == 0 {
return PublishStatus::NoClients;
}
self.frame_count += 1;
if self.use_zero_copy {
self.publish_zero_copy(texture, device, queue)
} else {
self.publish_cpu_fallback(texture, device, queue);
PublishStatus::CpuFallback
}
}
#[cfg(not(target_os = "macos"))]
PublishStatus::NoClients
}
#[cfg(target_os = "macos")]
fn publish_zero_copy(
&mut self,
texture: &wgpu::Texture,
device: &wgpu::Device,
_queue: &wgpu::Queue,
) -> PublishStatus {
let surface = match self.surface_pool.acquire() {
Some(s) => s,
None => {
log::warn!(
"[SyphonWgpuOutput] IOSurface pool exhausted — frame dropped. \
Consider increasing pool_size in SyphonOutputConfig."
);
return PublishStatus::PoolExhausted;
}
};
let mut published = false;
let _ = device.poll(wgpu::PollType::wait_indefinitely());
objc2::rc::autoreleasepool(|_| {
wgpu_interop::with_metal_texture(texture, |src_texture| {
let Some(src_texture) = src_texture else { return };
let Some((cmd_buf, dest_texture)) = self.metal_ctx.blit_to_iosurface(
src_texture, &surface, self.width, self.height
) else { return };
let tex_ptr = &*dest_texture as *const ProtocolObject<dyn MTLTexture> as *mut AnyObject;
let cmd_ptr = &*cmd_buf as *const ProtocolObject<dyn MTLCommandBuffer> as *mut AnyObject;
unsafe { self.server.publish_metal_texture(tex_ptr, cmd_ptr) };
cmd_buf.commit();
published = true;
});
});
self.surface_pool.release(surface);
if published { PublishStatus::ZeroCopy } else { PublishStatus::CpuFallback }
}
#[cfg(target_os = "macos")]
fn publish_cpu_fallback(&mut self, texture: &wgpu::Texture, device: &wgpu::Device, queue: &wgpu::Queue) {
let buffer_size = (self.width * self.height * 4) as u64;
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Syphon Staging"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Syphon Copy"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(self.width * 4),
rows_per_image: Some(self.height),
},
},
wgpu::Extent3d {
width: self.width,
height: self.height,
depth_or_array_layers: 1,
},
);
queue.submit(std::iter::once(encoder.finish()));
let _ = device.poll(wgpu::PollType::wait_indefinitely());
let buffer_slice = buffer.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
let _ = tx.send(result.is_ok());
});
let start = std::time::Instant::now();
let mut ready = false;
while start.elapsed().as_millis() < 10 {
if let Ok(true) = rx.try_recv() {
ready = true;
break;
}
std::thread::sleep(std::time::Duration::from_micros(100));
let _ = device.poll(wgpu::PollType::Poll);
}
if ready {
let data = buffer_slice.get_mapped_range();
if data.iter().any(|&b| b != 0) {
let upload = unsafe {
let desc = MTLTextureDescriptor::texture2DDescriptorWithPixelFormat_width_height_mipmapped(
MTLPixelFormat::BGRA8Unorm,
self.width as usize,
self.height as usize,
false,
);
desc.setStorageMode(MTLStorageMode::Managed);
desc.setUsage(MTLTextureUsage::ShaderRead);
self.metal_ctx.device().newTextureWithDescriptor(&desc)
};
if let (Some(mtl_texture), Some(cmd_buf)) =
(upload, self.metal_ctx.queue().commandBuffer())
{
unsafe {
mtl_texture.replaceRegion_mipmapLevel_withBytes_bytesPerRow(
MTLRegion {
origin: MTLOrigin { x: 0, y: 0, z: 0 },
size: MTLSize {
width: self.width as usize,
height: self.height as usize,
depth: 1,
},
},
0,
std::ptr::NonNull::new_unchecked(data.as_ptr() as *mut _),
(self.width * 4) as usize,
);
let texture_ptr = &*mtl_texture as *const ProtocolObject<dyn MTLTexture> as *mut AnyObject;
let cmd_buf_ptr = &*cmd_buf as *const ProtocolObject<dyn MTLCommandBuffer> as *mut AnyObject;
self.server.publish_metal_texture(texture_ptr, cmd_buf_ptr);
}
cmd_buf.commit();
}
}
drop(data);
buffer.unmap();
}
}
pub fn client_count(&self) -> usize {
self.server.client_count()
}
pub fn has_clients(&self) -> bool {
self.server.client_count() > 0
}
pub fn name(&self) -> &str {
self.server.name()
}
pub fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
#[cfg(target_os = "macos")]
pub fn is_zero_copy(&self) -> bool {
self.use_zero_copy
}
#[cfg(not(target_os = "macos"))]
pub fn is_zero_copy(&self) -> bool {
false
}
}
pub fn list_servers() -> Vec<String> {
syphon_core::SyphonServerDirectory::servers()
.into_iter()
.map(|info| info.name)
.collect()
}
pub fn is_available() -> bool {
syphon_core::is_available()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_availability() {
println!("Syphon available: {}", is_available());
}
}