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.
367 lines
12 KiB
Rust
367 lines
12 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
|
|
use crate::{MAX_FRAME_HEIGHT, MAX_FRAME_WIDTH, nv12_byte_size};
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum TextureFormat {
|
|
Nv12,
|
|
P010,
|
|
Bgra8,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum BackendError {
|
|
DimensionsOutOfRange { width: u32, height: u32 },
|
|
UnsupportedFormat { format: TextureFormat },
|
|
PlatformUnsupported { reason: &'static str },
|
|
KeyMismatch { expected: u64, observed: u64 },
|
|
AcquireWhileWriting { slot_index: u32 },
|
|
ReleaseWithoutAcquire { slot_index: u32 },
|
|
WouldBlock { slot_index: u32 },
|
|
}
|
|
|
|
impl std::fmt::Display for BackendError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::DimensionsOutOfRange { width, height } => {
|
|
write!(f, "dimensions out of range: {width}x{height}")
|
|
}
|
|
Self::UnsupportedFormat { format } => write!(f, "unsupported format: {format:?}"),
|
|
Self::PlatformUnsupported { reason } => write!(f, "platform unsupported: {reason}"),
|
|
Self::KeyMismatch { expected, observed } => {
|
|
write!(
|
|
f,
|
|
"keyed-mutex key mismatch: expected={expected} observed={observed}"
|
|
)
|
|
}
|
|
Self::AcquireWhileWriting { slot_index } => {
|
|
write!(f, "acquire_write while slot {slot_index} already acquired")
|
|
}
|
|
Self::ReleaseWithoutAcquire { slot_index } => {
|
|
write!(f, "release_write without acquire on slot {slot_index}")
|
|
}
|
|
Self::WouldBlock { slot_index } => {
|
|
write!(
|
|
f,
|
|
"keyed mutex busy on slot {slot_index}; skipped without blocking"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for BackendError {}
|
|
|
|
pub const NUM_SLOTS_DEFAULT: usize = 8;
|
|
|
|
pub trait KeyedMutexBackend: Send {
|
|
type SlotHandle: Send + Clone;
|
|
const NUM_SLOTS: usize;
|
|
|
|
fn create_slots(
|
|
&mut self,
|
|
width: u32,
|
|
height: u32,
|
|
format: TextureFormat,
|
|
) -> Result<Vec<Self::SlotHandle>, BackendError>;
|
|
|
|
fn acquire_write(&mut self, slot: &Self::SlotHandle, key: u64) -> Result<(), BackendError>;
|
|
|
|
fn release_write(&mut self, slot: &Self::SlotHandle, next_key: u64)
|
|
-> Result<(), BackendError>;
|
|
|
|
fn poll_complete(&mut self, slot: &Self::SlotHandle) -> bool;
|
|
|
|
fn mark_consumed(&mut self, slot: &Self::SlotHandle);
|
|
|
|
fn fill_test_pattern(&mut self, _slot: &Self::SlotHandle, _value: u8) {}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct CpuSlotHandle {
|
|
inner: Arc<CpuSlotInner>,
|
|
}
|
|
|
|
impl CpuSlotHandle {
|
|
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.buffer.len() == self.inner.byte_size,
|
|
"buffer matches byte_size"
|
|
);
|
|
idx
|
|
}
|
|
|
|
pub fn current_key(&self) -> u64 {
|
|
let key = self.inner.current_key.load(Ordering::Acquire);
|
|
assert!(
|
|
self.inner.buffer.len() == self.inner.byte_size,
|
|
"buffer intact"
|
|
);
|
|
assert!(
|
|
(self.inner.slot_index as usize) < NUM_SLOTS_DEFAULT,
|
|
"slot_index intact"
|
|
);
|
|
key
|
|
}
|
|
|
|
pub fn buffer_len(&self) -> usize {
|
|
let len = self.inner.byte_size;
|
|
assert!(len > 0, "byte_size positive");
|
|
assert!(self.inner.buffer.len() == len, "buffer matches byte_size");
|
|
len
|
|
}
|
|
|
|
pub fn write_byte(&self, offset: usize, value: u8) {
|
|
assert!(offset < self.inner.byte_size, "offset within buffer");
|
|
assert!(
|
|
self.inner.acquired.load(Ordering::Acquire),
|
|
"writes only while acquired",
|
|
);
|
|
unsafe {
|
|
let ptr = self.inner.buffer.as_ptr().add(offset) as *mut u8;
|
|
ptr.write_volatile(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CpuSlotInner {
|
|
slot_index: u32,
|
|
byte_size: usize,
|
|
buffer: Vec<u8>,
|
|
current_key: AtomicU64,
|
|
acquired: AtomicBool,
|
|
completed: AtomicBool,
|
|
}
|
|
|
|
pub struct CpuMemcpyBackend {
|
|
width: u32,
|
|
height: u32,
|
|
format: TextureFormat,
|
|
slots_created: bool,
|
|
slot_count: u32,
|
|
}
|
|
|
|
impl CpuMemcpyBackend {
|
|
pub fn new() -> Self {
|
|
let backend = Self {
|
|
width: 0,
|
|
height: 0,
|
|
format: TextureFormat::Nv12,
|
|
slots_created: false,
|
|
slot_count: 0,
|
|
};
|
|
assert!(!backend.slots_created, "fresh backend has no slots");
|
|
assert_eq!(backend.slot_count, 0, "fresh slot_count zero");
|
|
backend
|
|
}
|
|
|
|
pub fn width(&self) -> u32 {
|
|
assert!(self.width <= MAX_FRAME_WIDTH, "width within cap");
|
|
self.width
|
|
}
|
|
|
|
pub fn height(&self) -> u32 {
|
|
assert!(self.height <= MAX_FRAME_HEIGHT, "height within cap");
|
|
self.height
|
|
}
|
|
}
|
|
|
|
impl Default for CpuMemcpyBackend {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl KeyedMutexBackend for CpuMemcpyBackend {
|
|
type SlotHandle = CpuSlotHandle;
|
|
const NUM_SLOTS: usize = NUM_SLOTS_DEFAULT;
|
|
|
|
fn create_slots(
|
|
&mut self,
|
|
width: u32,
|
|
height: u32,
|
|
format: TextureFormat,
|
|
) -> Result<Vec<CpuSlotHandle>, 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 byte_size = nv12_byte_size(width, height);
|
|
assert!(byte_size > 0, "byte_size positive");
|
|
let mut out: Vec<CpuSlotHandle> = Vec::with_capacity(Self::NUM_SLOTS);
|
|
for idx in 0..Self::NUM_SLOTS {
|
|
let inner = CpuSlotInner {
|
|
slot_index: idx as u32,
|
|
byte_size,
|
|
buffer: vec![0u8; byte_size],
|
|
current_key: AtomicU64::new(0),
|
|
acquired: AtomicBool::new(false),
|
|
completed: AtomicBool::new(false),
|
|
};
|
|
out.push(CpuSlotHandle {
|
|
inner: Arc::new(inner),
|
|
});
|
|
}
|
|
self.width = width;
|
|
self.height = height;
|
|
self.format = format;
|
|
self.slots_created = true;
|
|
self.slot_count = Self::NUM_SLOTS as u32;
|
|
assert_eq!(out.len(), Self::NUM_SLOTS, "slot vector length");
|
|
assert!(self.slots_created, "slots_created flipped");
|
|
Ok(out)
|
|
}
|
|
|
|
fn acquire_write(&mut self, slot: &CpuSlotHandle, 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,
|
|
});
|
|
}
|
|
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: &CpuSlotHandle, 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,
|
|
});
|
|
}
|
|
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: &CpuSlotHandle) -> 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: &CpuSlotHandle) {
|
|
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);
|
|
}
|
|
|
|
fn fill_test_pattern(&mut self, slot: &CpuSlotHandle, value: u8) {
|
|
assert!(self.slots_created, "slots exist for fill");
|
|
assert!(
|
|
slot.inner.acquired.load(Ordering::Acquire),
|
|
"fill only while acquired"
|
|
);
|
|
let len = slot.inner.byte_size;
|
|
for offset in 0..len {
|
|
slot.write_byte(offset, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn create_slots_for_1080p_nv12_yields_eight_slots() {
|
|
let mut backend = CpuMemcpyBackend::new();
|
|
let slots = backend
|
|
.create_slots(1920, 1080, TextureFormat::Nv12)
|
|
.expect("creation succeeds");
|
|
assert_eq!(slots.len(), CpuMemcpyBackend::NUM_SLOTS);
|
|
assert_eq!(slots.len(), 8);
|
|
for (idx, slot) in slots.iter().enumerate() {
|
|
assert_eq!(slot.slot_index(), idx as u32);
|
|
assert_eq!(slot.buffer_len(), 1920 * 1080 * 3 / 2);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn create_slots_rejects_zero_dims() {
|
|
let mut backend = CpuMemcpyBackend::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 = CpuMemcpyBackend::new();
|
|
let err = backend.create_slots(1920, 1080, TextureFormat::P010).err();
|
|
assert!(matches!(err, Some(BackendError::UnsupportedFormat { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn acquire_release_round_trip_marks_complete() {
|
|
let mut backend = CpuMemcpyBackend::new();
|
|
let slots = backend
|
|
.create_slots(64, 64, TextureFormat::Nv12)
|
|
.expect("create");
|
|
let slot = slots[0].clone();
|
|
backend.acquire_write(&slot, 0).expect("acquire");
|
|
backend.release_write(&slot, 1).expect("release");
|
|
assert!(backend.poll_complete(&slot));
|
|
backend.mark_consumed(&slot);
|
|
assert!(!backend.poll_complete(&slot));
|
|
}
|
|
|
|
#[test]
|
|
fn double_acquire_rejects() {
|
|
let mut backend = CpuMemcpyBackend::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 { .. })
|
|
));
|
|
}
|
|
}
|