use syphon_core::{SyphonClient, Result, ServerInfo};
#[cfg(target_os = "macos")]
use syphon_metal::wgpu_interop;
#[cfg(target_os = "macos")]
use objc2::runtime::ProtocolObject;
#[cfg(target_os = "macos")]
use objc2_metal::{
MTLBlitCommandEncoder, MTLCommandBuffer, MTLCommandEncoder, MTLCommandQueue, MTLOrigin,
MTLSize, MTLTexture,
};
pub struct SyphonWgpuInput {
client: Option<SyphonClient>,
connected_server: Option<String>,
output_texture: Option<wgpu::Texture>,
output_width: u32,
output_height: u32,
#[cfg(target_os = "macos")]
metal_ctx: Option<syphon_metal::MetalContext>,
}
impl SyphonWgpuInput {
pub fn new(device: &wgpu::Device, _queue: &wgpu::Queue) -> Self {
#[cfg(target_os = "macos")]
let metal_ctx = Self::build_metal_ctx(device);
Self {
client: None,
connected_server: None,
output_texture: None,
output_width: 0,
output_height: 0,
#[cfg(target_os = "macos")]
metal_ctx,
}
}
#[cfg(target_os = "macos")]
fn build_metal_ctx(device: &wgpu::Device) -> Option<syphon_metal::MetalContext> {
let ctx = syphon_metal::MetalContext::from_wgpu_device(device);
if ctx.is_none() {
log::warn!("[SyphonWgpuInput] wgpu device is not Metal-backed; will use CPU fallback");
}
ctx
}
pub fn connect(&mut self, server_name: &str) -> Result<()> {
log::info!("[SyphonWgpuInput] Connecting to '{}'", server_name);
let client = SyphonClient::connect(server_name)?;
self.client = Some(client);
self.connected_server = Some(server_name.to_string());
log::info!("[SyphonWgpuInput] Connected");
Ok(())
}
pub fn connect_by_info(&mut self, info: &ServerInfo) -> Result<()> {
log::info!("[SyphonWgpuInput] Connecting to '{}' (uuid={})", info.display_name(), info.uuid);
let client = SyphonClient::connect_by_info(info)?;
self.connected_server = Some(info.display_name().to_string());
self.client = Some(client);
log::info!("[SyphonWgpuInput] Connected");
Ok(())
}
pub fn connect_with_channel(
&mut self,
server_name: &str,
) -> Result<std::sync::mpsc::Receiver<()>> {
log::info!("[SyphonWgpuInput] Connecting to '{}' (push mode)", server_name);
let (client, rx) = SyphonClient::connect_with_channel(server_name)?;
self.connected_server = Some(server_name.to_string());
self.client = Some(client);
log::info!("[SyphonWgpuInput] Connected (push mode)");
Ok(rx)
}
pub fn connect_by_info_with_channel(
&mut self,
info: &ServerInfo,
) -> Result<std::sync::mpsc::Receiver<()>> {
log::info!("[SyphonWgpuInput] Connecting to '{}' (uuid={}, push mode)", info.display_name(), info.uuid);
let (client, rx) = SyphonClient::connect_by_info_with_channel(info)?;
self.connected_server = Some(info.display_name().to_string());
self.client = Some(client);
log::info!("[SyphonWgpuInput] Connected (push mode)");
Ok(rx)
}
pub fn disconnect(&mut self) {
self.client = None;
self.connected_server = None;
self.output_texture = None;
log::info!("[SyphonWgpuInput] Disconnected");
}
pub fn is_connected(&self) -> bool {
self.client.as_ref().is_some_and(|c| {
#[cfg(target_os = "macos")]
{ c.is_connected() }
#[cfg(not(target_os = "macos"))]
{ true }
})
}
pub fn receive_texture(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
) -> bool {
let client = match self.client.as_ref() {
Some(c) => c,
None => return false,
};
#[cfg(target_os = "macos")]
{
if !client.has_new_frame() { return false; }
let mut frame = match client.try_receive() {
Ok(Some(f)) => f,
_ => return false,
};
let w = frame.width;
let h = frame.height;
if self.output_texture.is_none() || self.output_width != w || self.output_height != h {
log::info!("[SyphonWgpuInput] Creating output texture: {}x{}", w, h);
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("syphon_input"),
size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Bgra8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let zeros = vec![0u8; (w * h * 4) as usize];
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&zeros,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(w * 4),
rows_per_image: Some(h),
},
wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
);
self.output_texture = Some(tex);
self.output_width = w;
self.output_height = h;
}
let output = self.output_texture.as_ref().unwrap();
let used_gpu = if let Some(ref ctx) = self.metal_ctx {
let _ = device.poll(wgpu::PollType::wait_indefinitely());
Self::gpu_blit(&frame, output, ctx.queue())
} else {
false
};
if !used_gpu {
log::warn!("[SyphonWgpuInput] GPU blit unavailable, using CPU fallback");
let stride = frame.bytes_per_row() as u32;
let data = match frame.to_vec() {
Ok(d) => d,
Err(e) => { log::warn!("[SyphonWgpuInput] CPU read failed: {}", e); return false; }
};
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: output,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(stride),
rows_per_image: Some(h),
},
wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
);
}
true
}
#[cfg(not(target_os = "macos"))]
{ false }
}
pub fn output_texture(&self) -> Option<&wgpu::Texture> {
self.output_texture.as_ref()
}
#[cfg(target_os = "macos")]
fn gpu_blit(
frame: &syphon_core::Frame,
output: &wgpu::Texture,
metal_queue: &ProtocolObject<dyn MTLCommandQueue>,
) -> bool {
let frame_tex_ptr = frame.metal_texture_ptr();
if frame_tex_ptr.is_null() {
log::warn!("[SyphonWgpuInput] newFrameImage returned nil, cannot GPU blit");
return false;
}
let src: &ProtocolObject<dyn MTLTexture> =
unsafe { &*(frame_tex_ptr as *const ProtocolObject<dyn MTLTexture>) };
let mut ok = false;
objc2::rc::autoreleasepool(|_| {
wgpu_interop::with_metal_texture(output, |dst| {
let Some(dst) = dst else { return };
let Some(cmd) = metal_queue.commandBuffer() else { return };
let Some(enc) = cmd.blitCommandEncoder() else { return };
unsafe {
enc.copyFromTexture_sourceSlice_sourceLevel_sourceOrigin_sourceSize_toTexture_destinationSlice_destinationLevel_destinationOrigin(
src,
0, 0,
MTLOrigin { x: 0, y: 0, z: 0 },
MTLSize {
width: frame.width as usize,
height: frame.height as usize,
depth: 1,
},
dst,
0, 0,
MTLOrigin { x: 0, y: 0, z: 0 },
);
}
enc.endEncoding();
cmd.commit();
ok = true;
});
});
ok
}
pub fn server_name(&self) -> Option<&str> {
self.connected_server.as_deref()
}
}
impl Drop for SyphonWgpuInput {
fn drop(&mut self) {
self.disconnect();
}
}