Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 + tools/ci) with a "Connect to a Different Server" menu item and popout that lets the desktop app switch to any self-hosted Fluxer instance, plus fixes for well-known discovery on single-domain self-hosted deployments and a false-positive ERR_ABORTED on same-origin client redirects during the switch. Defaults to chat.fluxr.chat and uses an isolated userData directory from the official build.
448 lines
14 KiB
Rust
448 lines
14 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
|
|
use crate::backend::{BackendError, KeyedMutexBackend, NUM_SLOTS_DEFAULT, TextureFormat};
|
|
use crate::{MAX_FRAME_HEIGHT, MAX_FRAME_WIDTH};
|
|
|
|
pub const METAL_IOSURFACE_SEED_BASE: u64 = 0;
|
|
|
|
#[derive(Clone)]
|
|
pub struct IoSurfaceSlotHandle {
|
|
inner: Arc<IoSurfaceSlotInner>,
|
|
}
|
|
|
|
impl IoSurfaceSlotHandle {
|
|
pub fn slot_index(&self) -> u32 {
|
|
let idx = self.inner.slot_index;
|
|
assert!((idx as usize) < NUM_SLOTS_DEFAULT, "slot_index in range");
|
|
assert!(
|
|
self.inner.iosurface_handle != 0,
|
|
"iosurface_handle non-zero"
|
|
);
|
|
idx
|
|
}
|
|
|
|
pub fn iosurface_handle(&self) -> u64 {
|
|
let h = self.inner.iosurface_handle;
|
|
assert!(h != 0, "iosurface_handle non-zero on read");
|
|
assert!(
|
|
(self.inner.slot_index as usize) < NUM_SLOTS_DEFAULT,
|
|
"slot_index intact"
|
|
);
|
|
h
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
pub fn iosurface_ptr(&self) -> *mut core::ffi::c_void {
|
|
let p = self.inner.iosurface_ptr;
|
|
assert!(!p.is_null(), "iosurface_ptr non-null on read");
|
|
assert!(
|
|
self.inner.iosurface_handle != 0,
|
|
"handle non-zero alongside ptr"
|
|
);
|
|
p
|
|
}
|
|
|
|
pub fn current_key(&self) -> u64 {
|
|
let key = self.inner.current_key.load(Ordering::Acquire);
|
|
assert!(
|
|
self.inner.iosurface_handle != 0,
|
|
"handle intact during key read"
|
|
);
|
|
assert!(
|
|
(self.inner.slot_index as usize) < NUM_SLOTS_DEFAULT,
|
|
"slot_index intact"
|
|
);
|
|
key
|
|
}
|
|
|
|
pub fn is_acquired(&self) -> bool {
|
|
let a = self.inner.acquired.load(Ordering::Acquire);
|
|
assert!(
|
|
self.inner.iosurface_handle != 0,
|
|
"handle intact during acquired probe"
|
|
);
|
|
assert!(
|
|
(self.inner.slot_index as usize) < NUM_SLOTS_DEFAULT,
|
|
"slot_index intact"
|
|
);
|
|
a
|
|
}
|
|
}
|
|
|
|
struct IoSurfaceSlotInner {
|
|
slot_index: u32,
|
|
iosurface_handle: u64,
|
|
#[cfg(target_os = "macos")]
|
|
iosurface_ptr: *mut core::ffi::c_void,
|
|
current_key: AtomicU64,
|
|
acquired: AtomicBool,
|
|
completed: AtomicBool,
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
unsafe impl Send for IoSurfaceSlotInner {}
|
|
#[cfg(target_os = "macos")]
|
|
unsafe impl Sync for IoSurfaceSlotInner {}
|
|
|
|
pub struct MetalSharedTextureBackend {
|
|
width: u32,
|
|
height: u32,
|
|
format: TextureFormat,
|
|
slots_created: bool,
|
|
slot_handles: Vec<u64>,
|
|
#[cfg(target_os = "macos")]
|
|
iosurfaces: Vec<crate::metal_iosurface_macos::OwnedIoSurface>,
|
|
}
|
|
|
|
impl MetalSharedTextureBackend {
|
|
pub fn new() -> Self {
|
|
let backend = Self {
|
|
width: 0,
|
|
height: 0,
|
|
format: TextureFormat::Nv12,
|
|
slots_created: false,
|
|
slot_handles: Vec::with_capacity(NUM_SLOTS_DEFAULT),
|
|
#[cfg(target_os = "macos")]
|
|
iosurfaces: Vec::with_capacity(NUM_SLOTS_DEFAULT),
|
|
};
|
|
assert!(!backend.slots_created, "fresh backend has no slots");
|
|
assert!(
|
|
backend.slot_handles.is_empty(),
|
|
"fresh backend handles empty"
|
|
);
|
|
backend
|
|
}
|
|
|
|
pub fn width(&self) -> u32 {
|
|
assert!(self.width <= MAX_FRAME_WIDTH, "width within cap");
|
|
assert!(self.height <= MAX_FRAME_HEIGHT, "height within cap");
|
|
self.width
|
|
}
|
|
|
|
pub fn height(&self) -> u32 {
|
|
assert!(self.height <= MAX_FRAME_HEIGHT, "height within cap");
|
|
assert!(self.width <= MAX_FRAME_WIDTH, "width within cap");
|
|
self.height
|
|
}
|
|
|
|
pub fn slot_iosurface_handle(&self, slot_index: u32) -> Option<u64> {
|
|
let idx = slot_index as usize;
|
|
if idx >= self.slot_handles.len() {
|
|
return None;
|
|
}
|
|
let h = self.slot_handles[idx];
|
|
assert!(h != 0, "stored iosurface handle non-zero");
|
|
Some(h)
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
pub fn slot_iosurface_ptr(&self, slot_index: u32) -> Option<*mut core::ffi::c_void> {
|
|
let idx = slot_index as usize;
|
|
if idx >= self.iosurfaces.len() {
|
|
return None;
|
|
}
|
|
let p = self.iosurfaces[idx].as_ptr();
|
|
assert!(!p.is_null(), "stored iosurface ptr non-null");
|
|
assert!(
|
|
idx < self.slot_handles.len(),
|
|
"ptr slot mirrors handle slot"
|
|
);
|
|
Some(p)
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
pub fn slot_iosurface_mut(
|
|
&mut self,
|
|
slot_index: u32,
|
|
) -> Option<&mut crate::metal_iosurface_macos::OwnedIoSurface> {
|
|
let idx = slot_index as usize;
|
|
if idx >= self.iosurfaces.len() {
|
|
return None;
|
|
}
|
|
assert!(
|
|
idx < self.slot_handles.len(),
|
|
"mut slot mirrors handle slot"
|
|
);
|
|
Some(&mut self.iosurfaces[idx])
|
|
}
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
fn allocate_slot_handles(
|
|
&mut self,
|
|
_width: u32,
|
|
_height: u32,
|
|
_format: TextureFormat,
|
|
) -> Result<Vec<u64>, BackendError> {
|
|
Err(BackendError::PlatformUnsupported {
|
|
reason: "MetalSharedTextureBackend requires macOS",
|
|
})
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
fn allocate_slot_handles(
|
|
&mut self,
|
|
width: u32,
|
|
height: u32,
|
|
format: TextureFormat,
|
|
) -> Result<Vec<u64>, BackendError> {
|
|
if !matches!(format, TextureFormat::Nv12) {
|
|
return Err(BackendError::UnsupportedFormat { format });
|
|
}
|
|
let mut handles: Vec<u64> = Vec::with_capacity(NUM_SLOTS_DEFAULT);
|
|
let mut owned: Vec<crate::metal_iosurface_macos::OwnedIoSurface> =
|
|
Vec::with_capacity(NUM_SLOTS_DEFAULT);
|
|
for slot in 0..NUM_SLOTS_DEFAULT {
|
|
let surface = crate::metal_iosurface_macos::OwnedIoSurface::create_nv12(width, height)
|
|
.map_err(|_| BackendError::PlatformUnsupported {
|
|
reason: "IOSurfaceCreate failed",
|
|
})?;
|
|
let raw = surface.handle();
|
|
assert!(raw != 0, "IOSurface raw non-zero for slot");
|
|
assert!(slot < NUM_SLOTS_DEFAULT, "slot index in range");
|
|
handles.push(raw);
|
|
owned.push(surface);
|
|
}
|
|
self.iosurfaces = owned;
|
|
Ok(handles)
|
|
}
|
|
}
|
|
|
|
impl Default for MetalSharedTextureBackend {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl KeyedMutexBackend for MetalSharedTextureBackend {
|
|
type SlotHandle = IoSurfaceSlotHandle;
|
|
const NUM_SLOTS: usize = NUM_SLOTS_DEFAULT;
|
|
|
|
fn create_slots(
|
|
&mut self,
|
|
width: u32,
|
|
height: u32,
|
|
format: TextureFormat,
|
|
) -> Result<Vec<IoSurfaceSlotHandle>, BackendError> {
|
|
if width == 0 || height == 0 || width > MAX_FRAME_WIDTH || height > MAX_FRAME_HEIGHT {
|
|
return Err(BackendError::DimensionsOutOfRange { width, height });
|
|
}
|
|
if !matches!(format, TextureFormat::Nv12) {
|
|
return Err(BackendError::UnsupportedFormat { format });
|
|
}
|
|
assert!(!self.slots_created, "slots created once");
|
|
let raw_handles = self.allocate_slot_handles(width, height, format)?;
|
|
assert_eq!(
|
|
raw_handles.len(),
|
|
NUM_SLOTS_DEFAULT,
|
|
"allocator returns NUM_SLOTS handles"
|
|
);
|
|
let mut handles: Vec<IoSurfaceSlotHandle> = Vec::with_capacity(NUM_SLOTS_DEFAULT);
|
|
for (idx, raw) in raw_handles.iter().enumerate() {
|
|
assert!(*raw != 0, "raw iosurface handle non-zero");
|
|
#[cfg(target_os = "macos")]
|
|
let surface_ptr = self.iosurfaces[idx].as_ptr();
|
|
#[cfg(target_os = "macos")]
|
|
assert!(!surface_ptr.is_null(), "surface ptr non-null at slot setup");
|
|
let inner = IoSurfaceSlotInner {
|
|
slot_index: idx as u32,
|
|
iosurface_handle: *raw,
|
|
#[cfg(target_os = "macos")]
|
|
iosurface_ptr: surface_ptr,
|
|
current_key: AtomicU64::new(METAL_IOSURFACE_SEED_BASE),
|
|
acquired: AtomicBool::new(false),
|
|
completed: AtomicBool::new(false),
|
|
};
|
|
handles.push(IoSurfaceSlotHandle {
|
|
inner: Arc::new(inner),
|
|
});
|
|
}
|
|
self.width = width;
|
|
self.height = height;
|
|
self.format = format;
|
|
self.slot_handles = raw_handles;
|
|
self.slots_created = true;
|
|
assert_eq!(
|
|
handles.len(),
|
|
NUM_SLOTS_DEFAULT,
|
|
"returned handle vector length"
|
|
);
|
|
assert!(self.slots_created, "slots_created flipped");
|
|
Ok(handles)
|
|
}
|
|
|
|
fn acquire_write(&mut self, slot: &IoSurfaceSlotHandle, key: u64) -> Result<(), BackendError> {
|
|
assert!(self.slots_created, "slots must exist before acquire");
|
|
let current = slot.inner.current_key.load(Ordering::Acquire);
|
|
if current != key {
|
|
return Err(BackendError::KeyMismatch {
|
|
expected: key,
|
|
observed: current,
|
|
});
|
|
}
|
|
let was_acquired = slot.inner.acquired.swap(true, Ordering::AcqRel);
|
|
if was_acquired {
|
|
return Err(BackendError::AcquireWhileWriting {
|
|
slot_index: slot.inner.slot_index,
|
|
});
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let idx = slot.inner.slot_index as usize;
|
|
if idx >= self.iosurfaces.len() {
|
|
slot.inner.acquired.store(false, Ordering::Release);
|
|
return Err(BackendError::PlatformUnsupported {
|
|
reason: "slot index out of range for IOSurface vector",
|
|
});
|
|
}
|
|
if let Err(e) = self.iosurfaces[idx].lock_for_writing() {
|
|
slot.inner.acquired.store(false, Ordering::Release);
|
|
return Err(e);
|
|
}
|
|
}
|
|
slot.inner.completed.store(false, Ordering::Release);
|
|
assert!(
|
|
slot.inner.acquired.load(Ordering::Acquire),
|
|
"acquired flag set"
|
|
);
|
|
assert!(
|
|
!slot.inner.completed.load(Ordering::Acquire),
|
|
"completed cleared"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn release_write(
|
|
&mut self,
|
|
slot: &IoSurfaceSlotHandle,
|
|
next_key: u64,
|
|
) -> Result<(), BackendError> {
|
|
assert!(self.slots_created, "slots must exist before release");
|
|
let was_acquired = slot.inner.acquired.swap(false, Ordering::AcqRel);
|
|
if !was_acquired {
|
|
return Err(BackendError::ReleaseWithoutAcquire {
|
|
slot_index: slot.inner.slot_index,
|
|
});
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let idx = slot.inner.slot_index as usize;
|
|
if idx >= self.iosurfaces.len() {
|
|
return Err(BackendError::PlatformUnsupported {
|
|
reason: "slot index out of range for IOSurface vector",
|
|
});
|
|
}
|
|
self.iosurfaces[idx].unlock_after_writing()?;
|
|
}
|
|
slot.inner.current_key.store(next_key, Ordering::Release);
|
|
slot.inner.completed.store(true, Ordering::Release);
|
|
assert!(
|
|
!slot.inner.acquired.load(Ordering::Acquire),
|
|
"acquired cleared"
|
|
);
|
|
assert!(
|
|
slot.inner.completed.load(Ordering::Acquire),
|
|
"completed flag set"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn poll_complete(&mut self, slot: &IoSurfaceSlotHandle) -> bool {
|
|
let done = slot.inner.completed.load(Ordering::Acquire);
|
|
assert!(self.slots_created, "slots exist for poll");
|
|
assert!(
|
|
(slot.inner.slot_index as usize) < Self::NUM_SLOTS,
|
|
"slot index in range"
|
|
);
|
|
done
|
|
}
|
|
|
|
fn mark_consumed(&mut self, slot: &IoSurfaceSlotHandle) {
|
|
assert!(self.slots_created, "slots exist for mark_consumed");
|
|
assert!(
|
|
(slot.inner.slot_index as usize) < Self::NUM_SLOTS,
|
|
"slot index in range"
|
|
);
|
|
slot.inner.completed.store(false, Ordering::Release);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn off_macos_returns_platform_unsupported() {
|
|
let mut backend = MetalSharedTextureBackend::new();
|
|
let result = backend.create_slots(1920, 1080, TextureFormat::Nv12);
|
|
#[cfg(not(target_os = "macos"))]
|
|
{
|
|
assert!(matches!(
|
|
result,
|
|
Err(BackendError::PlatformUnsupported { .. })
|
|
));
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let slots = result.expect("macos create_slots ok");
|
|
assert_eq!(slots.len(), 8);
|
|
for (idx, s) in slots.iter().enumerate() {
|
|
assert_eq!(s.slot_index(), idx as u32);
|
|
assert!(s.iosurface_handle() != 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn create_slots_rejects_zero_dims() {
|
|
let mut backend = MetalSharedTextureBackend::new();
|
|
let err = backend.create_slots(0, 1080, TextureFormat::Nv12).err();
|
|
assert!(matches!(
|
|
err,
|
|
Some(BackendError::DimensionsOutOfRange { .. })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn create_slots_rejects_unsupported_format() {
|
|
let mut backend = MetalSharedTextureBackend::new();
|
|
let err = backend.create_slots(1920, 1080, TextureFormat::P010).err();
|
|
assert!(matches!(err, Some(BackendError::UnsupportedFormat { .. })));
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
#[test]
|
|
fn macos_acquire_release_round_trip_marks_complete() {
|
|
let mut backend = MetalSharedTextureBackend::new();
|
|
let slots = backend
|
|
.create_slots(64, 64, TextureFormat::Nv12)
|
|
.expect("create");
|
|
let slot = slots[0].clone();
|
|
backend.acquire_write(&slot, 0).expect("acquire");
|
|
assert!(slot.is_acquired());
|
|
backend.release_write(&slot, 1).expect("release");
|
|
assert!(!slot.is_acquired());
|
|
assert!(backend.poll_complete(&slot));
|
|
backend.mark_consumed(&slot);
|
|
assert!(!backend.poll_complete(&slot));
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
#[test]
|
|
fn macos_double_acquire_rejects() {
|
|
let mut backend = MetalSharedTextureBackend::new();
|
|
let slots = backend
|
|
.create_slots(64, 64, TextureFormat::Nv12)
|
|
.expect("create");
|
|
let slot = slots[0].clone();
|
|
backend.acquire_write(&slot, 0).expect("first acquire");
|
|
let err = backend.acquire_write(&slot, 0).err();
|
|
assert!(matches!(
|
|
err,
|
|
Some(BackendError::AcquireWhileWriting { .. })
|
|
));
|
|
}
|
|
}
|