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,
MTLSharedEvent, MTLSize, MTLTexture,
};
#[cfg(target_os = "macos")]
use syphon_metal::wgpu_interop::SharedEvent;
#[cfg(target_os = "macos")]
struct EventSync {
blit_done: SharedEvent,
wgpu_done: SharedEvent,
frame: u64,
}
#[cfg(target_os = "macos")]
impl EventSync {
fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Option<Self> {
let blit_done = syphon_metal::wgpu_interop::new_shared_event(device)?;
let wgpu_done = syphon_metal::wgpu_interop::new_shared_event(device)?;
if !syphon_metal::wgpu_interop::queue_is_metal(queue) {
log::warn!(
"[SyphonWgpuInput] queue cannot carry shared events; using the CPU drain"
);
return None;
}
Some(Self { blit_done, wgpu_done, frame: 0 })
}
}
#[cfg(target_os = "macos")]
struct BlitSync<'a> {
wgpu_done: &'a ProtocolObject<dyn MTLSharedEvent>,
wait_value: u64,
blit_done: &'a ProtocolObject<dyn MTLSharedEvent>,
signal_value: u64,
}
#[cfg(target_os = "macos")]
fn event_sync_enabled() -> bool {
use std::sync::OnceLock;
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| {
matches!(std::env::var("SYPHON_WGPU_SYNC").as_deref(), Ok("event"))
})
}
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>,
#[cfg(target_os = "macos")]
sync: Option<EventSync>,
}
impl SyphonWgpuInput {
pub fn new(device: &wgpu::Device, _queue: &wgpu::Queue) -> Self {
#[cfg(target_os = "macos")]
let metal_ctx = Self::build_metal_ctx(device);
#[cfg(target_os = "macos")]
let sync = if metal_ctx.is_some() && event_sync_enabled() {
EventSync::new(device, _queue)
} else {
None
};
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")]
sync,
}
}
#[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 = match (&self.metal_ctx, &mut self.sync) {
(Some(ctx), Some(sync)) => {
Self::blit_with_events(&frame, output, ctx.queue(), sync, device, queue)
}
(Some(ctx), None) => {
crate::drain_wgpu_before_blit(device, "receive_texture");
Self::gpu_blit(&frame, output, ctx.queue(), None)
}
(None, _) => 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>,
sync: Option<&BlitSync<'_>>,
) -> 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 };
if let Some(sync) = sync {
cmd.encodeWaitForEvent_value(ProtocolObject::from_ref(sync.wgpu_done), sync.wait_value);
}
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();
if let Some(sync) = sync {
cmd.encodeSignalEvent_value(ProtocolObject::from_ref(sync.blit_done), sync.signal_value);
}
cmd.commit();
ok = true;
});
});
ok
}
#[cfg(target_os = "macos")]
fn blit_with_events(
frame: &syphon_core::Frame,
output: &wgpu::Texture,
metal_queue: &ProtocolObject<dyn MTLCommandQueue>,
sync: &mut EventSync,
device: &wgpu::Device,
queue: &wgpu::Queue,
) -> bool {
let stalled = syphon_metal::wgpu_interop::queue_take_pending_signal(queue, &sync.wgpu_done);
if stalled {
log::debug!(
"[SyphonWgpuInput] wgpu has not submitted since the last frame; \
draining once and restarting the fence chain"
);
crate::drain_wgpu_before_blit(device, "receive_texture (resync)");
}
let n = sync.frame + 1;
let blit_sync = BlitSync {
wgpu_done: &sync.wgpu_done,
wait_value: if stalled { 0 } else { sync.frame },
blit_done: &sync.blit_done,
signal_value: n,
};
if !Self::gpu_blit(frame, output, metal_queue, Some(&blit_sync)) {
return false;
}
sync.frame = n;
if !syphon_metal::wgpu_interop::queue_wait_for_event(queue, &sync.blit_done, n)
|| !syphon_metal::wgpu_interop::queue_signal_event(queue, &sync.wgpu_done, n)
{
log::warn!("[SyphonWgpuInput] could not stage frame {n} events on wgpu's queue");
}
true
}
pub fn server_name(&self) -> Option<&str> {
self.connected_server.as_deref()
}
}
impl Drop for SyphonWgpuInput {
fn drop(&mut self) {
self.disconnect();
}
}