Add native self-hosted instance connection to fluxer_desktop
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.
This commit is contained in:
@@ -0,0 +1,447 @@
|
||||
// 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 { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user