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:
2026-07-01 18:22:43 -04:00
commit 682afacd30
1763 changed files with 613720 additions and 0 deletions
@@ -0,0 +1,403 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use parking_lot::Mutex;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(target_os = "linux")]
use std::os::fd::RawFd;
pub const FRAME_BYTES_MAX: usize = 1920 * 1080 * 4;
pub const MAX_CPU_FRAMES: usize = 8;
#[cfg(target_os = "macos")]
pub struct IoSurfaceHandle;
#[cfg(target_os = "windows")]
#[derive(Debug)]
pub struct D3D11Handle(pub usize);
#[cfg(target_os = "windows")]
unsafe impl Send for D3D11Handle {}
#[cfg(target_os = "windows")]
unsafe impl Sync for D3D11Handle {}
pub enum Frame {
Cpu(Box<[u8]>),
#[cfg(feature = "wgpu")]
WgpuTexture {
texture: wgpu::Texture,
format: wgpu::TextureFormat,
dims: (u32, u32),
},
#[cfg(target_os = "macos")]
IoSurface {
surface_ref: IoSurfaceHandle,
},
#[cfg(target_os = "linux")]
Dmabuf {
fds: Vec<RawFd>,
format_modifier: u64,
},
#[cfg(target_os = "windows")]
D3D11Shared {
handle: D3D11Handle,
key: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FramePoolError {
ZeroCapacity,
CapacityOverflow,
}
impl std::fmt::Display for FramePoolError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ZeroCapacity => write!(f, "frame pool capacity must be greater than zero"),
Self::CapacityOverflow => write!(f, "frame pool capacity exceeds usize bounds"),
}
}
}
impl std::error::Error for FramePoolError {}
struct PoolInner {
slots: Vec<Arc<Frame>>,
free: Mutex<Vec<usize>>,
capacity: usize,
acquired_total: AtomicU64,
skipped_total: AtomicU64,
currently_in_flight: AtomicU64,
}
pub struct FramePool {
inner: Arc<PoolInner>,
}
impl FramePool {
pub fn from_slots(slots: Vec<Arc<Frame>>) -> Result<Self, FramePoolError> {
let capacity = slots.len();
assert!(capacity == slots.len());
if capacity == 0 {
return Err(FramePoolError::ZeroCapacity);
}
assert!(capacity > 0);
let mut free = Vec::with_capacity(capacity);
for index in 0..capacity {
free.push(index);
}
assert_eq!(free.len(), capacity);
let inner = PoolInner {
slots,
free: Mutex::new(free),
capacity,
acquired_total: AtomicU64::new(0),
skipped_total: AtomicU64::new(0),
currently_in_flight: AtomicU64::new(0),
};
Ok(Self {
inner: Arc::new(inner),
})
}
pub fn try_acquire(&self) -> Option<PooledFrame> {
assert!(self.inner.capacity > 0);
let mut free = self.inner.free.lock();
assert!(free.len() <= self.inner.capacity);
let index = match free.pop() {
Some(idx) => idx,
None => {
drop(free);
self.inner.skipped_total.fetch_add(1, Ordering::Relaxed);
return None;
}
};
assert!(index < self.inner.capacity);
self.inner.acquired_total.fetch_add(1, Ordering::Relaxed);
let after = self
.inner
.currently_in_flight
.fetch_add(1, Ordering::AcqRel)
+ 1;
assert!(after as usize <= self.inner.capacity);
drop(free);
let frame = Arc::clone(&self.inner.slots[index]);
Some(PooledFrame {
frame,
slot_index: index,
pool: Arc::clone(&self.inner),
})
}
pub fn capacity(&self) -> usize {
assert!(self.inner.capacity > 0);
let cap = self.inner.capacity;
assert!(cap == self.inner.slots.len());
cap
}
pub fn acquired_total(&self) -> u64 {
assert!(self.inner.capacity > 0);
let total = self.inner.acquired_total.load(Ordering::Relaxed);
assert!(total >= self.inner.currently_in_flight.load(Ordering::Relaxed));
total
}
pub fn skipped_total(&self) -> u64 {
assert!(self.inner.capacity > 0);
let in_flight = self.inner.currently_in_flight.load(Ordering::Relaxed);
assert!(in_flight as usize <= self.inner.capacity);
self.inner.skipped_total.load(Ordering::Relaxed)
}
pub fn currently_in_flight(&self) -> u64 {
let in_flight = self.inner.currently_in_flight.load(Ordering::Acquire);
assert!(in_flight as usize <= self.inner.capacity);
assert!(in_flight <= self.inner.acquired_total.load(Ordering::Relaxed));
in_flight
}
}
pub struct PooledFrame {
frame: Arc<Frame>,
slot_index: usize,
pool: Arc<PoolInner>,
}
impl PooledFrame {
pub fn frame(&self) -> &Arc<Frame> {
assert!(self.slot_index < self.pool.capacity);
assert!(Arc::strong_count(&self.frame) >= 2);
&self.frame
}
pub fn slot_index(&self) -> usize {
assert!(self.slot_index < self.pool.capacity);
let idx = self.slot_index;
assert!(idx < self.pool.slots.len());
idx
}
}
impl Drop for PooledFrame {
fn drop(&mut self) {
assert!(self.slot_index < self.pool.capacity);
let mut free = self.pool.free.lock();
assert!(free.len() < self.pool.capacity);
let before = self.pool.currently_in_flight.load(Ordering::Acquire);
assert!(before >= 1);
assert!(before as usize <= self.pool.capacity);
free.push(self.slot_index);
let after = self.pool.currently_in_flight.fetch_sub(1, Ordering::AcqRel) - 1;
assert!(after as usize <= self.pool.capacity);
drop(free);
}
}
pub struct CpuFrameBuilder;
impl CpuFrameBuilder {
pub fn build_pool(bytes_per_slot: usize) -> Result<FramePool, FramePoolError> {
assert!(bytes_per_slot > 0);
assert!(bytes_per_slot <= FRAME_BYTES_MAX);
if bytes_per_slot == 0 {
return Err(FramePoolError::ZeroCapacity);
}
let mut slots: Vec<Arc<Frame>> = Vec::with_capacity(MAX_CPU_FRAMES);
for _ in 0..MAX_CPU_FRAMES {
let buf: Box<[u8]> = vec![0u8; bytes_per_slot].into_boxed_slice();
assert_eq!(buf.len(), bytes_per_slot);
slots.push(Arc::new(Frame::Cpu(buf)));
}
assert_eq!(slots.len(), MAX_CPU_FRAMES);
FramePool::from_slots(slots)
}
pub fn build_pool_with_capacity(
capacity: usize,
bytes_per_slot: usize,
) -> Result<FramePool, FramePoolError> {
assert!(bytes_per_slot <= FRAME_BYTES_MAX);
if capacity == 0 {
return Err(FramePoolError::ZeroCapacity);
}
assert!(capacity > 0);
let mut slots: Vec<Arc<Frame>> = Vec::with_capacity(capacity);
for _ in 0..capacity {
let buf: Box<[u8]> = vec![0u8; bytes_per_slot].into_boxed_slice();
assert_eq!(buf.len(), bytes_per_slot);
slots.push(Arc::new(Frame::Cpu(buf)));
}
assert_eq!(slots.len(), capacity);
FramePool::from_slots(slots)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Barrier;
use std::sync::atomic::AtomicUsize;
use std::thread;
use std::time::{Duration, Instant};
const SMALL_SLOT_BYTES: usize = 64;
fn small_pool(capacity: usize) -> FramePool {
CpuFrameBuilder::build_pool_with_capacity(capacity, SMALL_SLOT_BYTES)
.expect("non-zero capacity")
}
#[test]
fn acquire_up_to_capacity_then_returns_none() {
let pool = small_pool(4);
let mut held = Vec::new();
for expected in 0..4 {
let frame = pool.try_acquire().expect("slot must be available");
assert_eq!(pool.currently_in_flight() as usize, expected + 1);
held.push(frame);
}
assert_eq!(pool.acquired_total(), 4);
assert!(pool.try_acquire().is_none());
assert_eq!(pool.skipped_total(), 1);
assert_eq!(pool.currently_in_flight(), 4);
}
#[test]
fn drop_releases_slot_back_to_pool() {
let pool = small_pool(2);
let first = pool.try_acquire().expect("first slot");
let second = pool.try_acquire().expect("second slot");
assert!(pool.try_acquire().is_none());
drop(first);
let revived = pool.try_acquire().expect("released slot returns");
assert_eq!(pool.currently_in_flight(), 2);
drop(second);
drop(revived);
assert_eq!(pool.currently_in_flight(), 0);
}
#[test]
fn zero_capacity_pool_always_returns_none() {
match CpuFrameBuilder::build_pool_with_capacity(0, SMALL_SLOT_BYTES) {
Err(err) => assert_eq!(err, FramePoolError::ZeroCapacity),
Ok(_) => panic!("zero capacity must be rejected"),
}
match FramePool::from_slots(Vec::new()) {
Err(err) => assert_eq!(err, FramePoolError::ZeroCapacity),
Ok(_) => panic!("empty slot vec must be rejected"),
}
}
#[test]
fn held_frame_has_strong_count_two_then_one() {
let pool = small_pool(1);
let held = pool.try_acquire().expect("slot must be available");
assert_eq!(Arc::strong_count(held.frame()), 2);
drop(held);
let again = pool.try_acquire().expect("slot returned");
assert_eq!(Arc::strong_count(again.frame()), 2);
drop(again);
}
#[test]
fn multi_thread_acquire_release_stress_does_not_deadlock() {
const THREADS: usize = 10;
const OPS_PER_THREAD: usize = 1000;
const POOL_CAPACITY: usize = 4;
let pool = Arc::new(small_pool(POOL_CAPACITY));
let barrier = Arc::new(Barrier::new(THREADS));
let acquired_observed = Arc::new(AtomicUsize::new(0));
let skipped_observed = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::with_capacity(THREADS);
for _ in 0..THREADS {
let pool = Arc::clone(&pool);
let barrier = Arc::clone(&barrier);
let acquired_observed = Arc::clone(&acquired_observed);
let skipped_observed = Arc::clone(&skipped_observed);
handles.push(thread::spawn(move || {
barrier.wait();
let mut local_acquired: usize = 0;
let mut local_skipped: usize = 0;
for _ in 0..OPS_PER_THREAD {
match pool.try_acquire() {
Some(frame) => {
local_acquired += 1;
drop(frame);
}
None => {
local_skipped += 1;
}
}
}
acquired_observed.fetch_add(local_acquired, Ordering::Relaxed);
skipped_observed.fetch_add(local_skipped, Ordering::Relaxed);
}));
}
let deadline = Instant::now() + Duration::from_secs(30);
for handle in handles {
assert!(
Instant::now() < deadline,
"stress test exceeded 30s budget — likely deadlock"
);
handle.join().expect("worker panicked");
}
let total = pool.acquired_total() + pool.skipped_total();
assert_eq!(total as usize, THREADS * OPS_PER_THREAD);
assert_eq!(
pool.acquired_total() as usize,
acquired_observed.load(Ordering::Relaxed)
);
assert_eq!(
pool.skipped_total() as usize,
skipped_observed.load(Ordering::Relaxed)
);
assert_eq!(pool.currently_in_flight(), 0);
}
#[test]
fn skipped_counter_increments_only_on_empty_pool() {
let pool = small_pool(1);
let held = pool.try_acquire().expect("first slot");
assert!(pool.try_acquire().is_none());
assert!(pool.try_acquire().is_none());
assert_eq!(pool.skipped_total(), 2);
assert_eq!(pool.acquired_total(), 1);
drop(held);
let _again = pool.try_acquire().expect("slot returned");
assert_eq!(pool.acquired_total(), 2);
assert_eq!(pool.skipped_total(), 2);
}
#[test]
fn cpu_frame_builder_default_capacity_allocates_max_slots() {
let pool =
CpuFrameBuilder::build_pool(SMALL_SLOT_BYTES).expect("default builder must succeed");
assert_eq!(pool.capacity(), MAX_CPU_FRAMES);
let mut held = Vec::with_capacity(MAX_CPU_FRAMES);
for _ in 0..MAX_CPU_FRAMES {
held.push(pool.try_acquire().expect("slot in capacity"));
}
assert!(pool.try_acquire().is_none());
for frame in &held {
match frame.frame().as_ref() {
Frame::Cpu(buf) => assert_eq!(buf.len(), SMALL_SLOT_BYTES),
#[allow(unreachable_patterns)]
_ => panic!("CpuFrameBuilder must emit Frame::Cpu variants"),
}
}
}
}
@@ -0,0 +1,429 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::StagingBackend;
use fluxer_gpu_rebuild::{GpuLossCallback, GpuRebuildError};
pub const MIN_STAGING_BYTES: u64 = 1;
pub const MAX_STAGING_BYTES: u64 = 1 << 30;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WgpuStagingConfig {
pub byte_len: u64,
}
impl WgpuStagingConfig {
pub fn new(byte_len: u64) -> Self {
assert!(byte_len >= MIN_STAGING_BYTES, "byte_len must be positive");
assert!(byte_len <= MAX_STAGING_BYTES, "byte_len exceeds sanity cap");
Self { byte_len }
}
}
struct WgpuStagingResources {
buffer: wgpu::Buffer,
cpu_mirror: Vec<u8>,
ready: bool,
}
pub struct WgpuStagingBackend {
config: WgpuStagingConfig,
resources: Option<WgpuStagingResources>,
}
impl WgpuStagingBackend {
pub fn new(device: &wgpu::Device, config: WgpuStagingConfig) -> Self {
assert!(config.byte_len >= MIN_STAGING_BYTES, "config min invariant");
assert!(config.byte_len <= MAX_STAGING_BYTES, "config max invariant");
let resources = build_resources(device, config);
Self {
config,
resources: Some(resources),
}
}
pub fn new_unbuilt(config: WgpuStagingConfig) -> Self {
assert!(config.byte_len >= MIN_STAGING_BYTES, "config min invariant");
assert!(config.byte_len <= MAX_STAGING_BYTES, "config max invariant");
Self {
config,
resources: None,
}
}
pub fn config(&self) -> WgpuStagingConfig {
assert!(
self.config.byte_len >= MIN_STAGING_BYTES,
"config min invariant"
);
assert!(
self.config.byte_len <= MAX_STAGING_BYTES,
"config max invariant"
);
self.config
}
pub fn is_built(&self) -> bool {
let built = self.resources.is_some();
assert!(
self.config.byte_len >= MIN_STAGING_BYTES,
"config min while introspecting"
);
assert!(
self.config.byte_len <= MAX_STAGING_BYTES,
"config max while introspecting"
);
built
}
pub fn buffer(&self) -> Option<&wgpu::Buffer> {
let buf = self.resources.as_ref().map(|r| &r.buffer);
assert_eq!(
buf.is_some(),
self.is_built(),
"buffer presence must align with built state",
);
buf
}
}
impl StagingBackend for WgpuStagingBackend {
fn write<F: FnOnce(&mut [u8])>(&mut self, fill: F) {
let Some(resources) = self.resources.as_mut() else {
return;
};
fill(&mut resources.cpu_mirror);
resources.ready = true;
}
fn read<R, F: FnOnce(&[u8]) -> R>(&self, read: F) -> R {
let empty: &[u8] = &[];
match self.resources.as_ref() {
Some(r) => read(&r.cpu_mirror),
None => read(empty),
}
}
fn is_ready(&self) -> bool {
match self.resources.as_ref() {
Some(r) => r.ready,
None => false,
}
}
fn is_idle(&self) -> bool {
match self.resources.as_ref() {
Some(r) => !r.ready,
None => true,
}
}
}
impl GpuLossCallback for WgpuStagingBackend {
fn release(&mut self) {
assert!(
self.config.byte_len >= MIN_STAGING_BYTES,
"release config min invariant"
);
assert!(
self.config.byte_len <= MAX_STAGING_BYTES,
"release config max invariant"
);
self.resources = None;
assert!(!self.is_built(), "release postcondition: must be unbuilt");
}
fn rebuild(
&mut self,
device: &wgpu::Device,
_queue: &wgpu::Queue,
) -> Result<(), GpuRebuildError> {
assert!(
self.config.byte_len >= MIN_STAGING_BYTES,
"rebuild config min invariant"
);
assert!(
self.config.byte_len <= MAX_STAGING_BYTES,
"rebuild config max invariant"
);
if self.resources.is_some() {
return Err(GpuRebuildError::OwnerInvariantBroken {
reason: "rebuild without prior release",
});
}
let resources = build_resources(device, self.config);
self.resources = Some(resources);
assert!(self.is_built(), "rebuild postcondition: must be built");
Ok(())
}
fn is_ready(&self) -> bool {
self.is_built()
}
fn debug_label(&self) -> &'static str {
"screen_frame_bus.wgpu_staging_backend"
}
}
fn build_resources(device: &wgpu::Device, config: WgpuStagingConfig) -> WgpuStagingResources {
assert!(
config.byte_len >= MIN_STAGING_BYTES,
"build_resources min invariant"
);
assert!(
config.byte_len <= MAX_STAGING_BYTES,
"build_resources max invariant"
);
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("screen_frame_bus.wgpu_staging_buffer"),
size: config.byte_len,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let cpu_mirror = vec![0u8; config.byte_len as usize];
assert_eq!(
cpu_mirror.len() as u64,
config.byte_len,
"cpu mirror must match configured byte len",
);
WgpuStagingResources {
buffer,
cpu_mirror,
ready: false,
}
}
pub fn try_acquire_device() -> Option<(wgpu::Device, wgpu::Queue, wgpu::Instance)> {
let mut descriptor = wgpu::InstanceDescriptor::new_without_display_handle();
descriptor.backends = wgpu::Backends::all() | wgpu::Backends::SECONDARY;
let instance = wgpu::Instance::new(descriptor);
let adapter = pollster_block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
force_fallback_adapter: false,
compatible_surface: None,
}))
.ok()?;
let device_result = pollster_block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("screen_frame_bus.wgpu_staging_backend.test_device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
experimental_features: wgpu::ExperimentalFeatures::default(),
}));
match device_result {
Ok((device, queue)) => Some((device, queue, instance)),
Err(_) => None,
}
}
fn pollster_block_on<F: core::future::Future>(fut: F) -> F::Output {
futures_executor_block_on(fut)
}
fn futures_executor_block_on<F: core::future::Future>(mut fut: F) -> F::Output {
use core::pin::Pin;
use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
fn raw_waker() -> RawWaker {
fn no_op(_: *const ()) {}
fn clone(_: *const ()) -> RawWaker {
raw_waker()
}
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, no_op, no_op, no_op);
RawWaker::new(core::ptr::null(), &VTABLE)
}
let waker = unsafe { Waker::from_raw(raw_waker()) };
let mut cx = Context::from_waker(&waker);
let mut fut = unsafe { Pin::new_unchecked(&mut fut) };
loop {
match fut.as_mut().poll(&mut cx) {
Poll::Ready(out) => return out,
Poll::Pending => continue,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{CpuStagingBackend, STAGING_PAIR_LEN, StagingSurfacePair};
use fluxer_gpu_rebuild::{GpuLossRegistry, RebuildOutcome};
struct GpuCtx {
device: wgpu::Device,
queue: wgpu::Queue,
_instance: wgpu::Instance,
}
fn gpu_ctx() -> Option<GpuCtx> {
let acquired = std::panic::catch_unwind(std::panic::AssertUnwindSafe(try_acquire_device));
let (device, queue, instance) = match acquired {
Ok(Some(triple)) => triple,
Ok(None) => return None,
Err(_) => return None,
};
Some(GpuCtx {
device,
queue,
_instance: instance,
})
}
#[test]
fn staging_backend_is_ready_only_when_built() {
let Some(ctx) = gpu_ctx() else {
eprintln!("screen-frame-bus gpu_loss: no wgpu adapter, skipping");
return;
};
let cfg = WgpuStagingConfig::new(256);
let mut backend = WgpuStagingBackend::new(&ctx.device, cfg);
assert!(
GpuLossCallback::is_ready(&backend),
"freshly built backend must be ready",
);
backend.release();
assert!(
!GpuLossCallback::is_ready(&backend),
"released backend must not be ready",
);
let outcome = backend.rebuild(&ctx.device, &ctx.queue);
assert!(outcome.is_ok(), "rebuild on fresh device must succeed");
assert!(
GpuLossCallback::is_ready(&backend),
"rebuilt backend must be ready",
);
}
#[test]
fn registry_handles_staging_backend_round_trip() {
let Some(ctx) = gpu_ctx() else {
eprintln!("screen-frame-bus gpu_loss: no wgpu adapter, skipping");
return;
};
let registry = GpuLossRegistry::new();
let backend = Box::new(WgpuStagingBackend::new(
&ctx.device,
WgpuStagingConfig::new(128),
));
let _guard = registry.register(backend);
let report = registry.handle_device_lost(&ctx.device, &ctx.queue);
assert_eq!(report.released_count, 1);
assert_eq!(report.rebuilt_count, 1);
assert_eq!(report.failed_count, 0);
assert!(report.is_total_success());
}
#[test]
fn double_rebuild_without_release_is_owner_invariant_error() {
let Some(ctx) = gpu_ctx() else {
eprintln!("screen-frame-bus gpu_loss: no wgpu adapter, skipping");
return;
};
let mut backend = WgpuStagingBackend::new(&ctx.device, WgpuStagingConfig::new(64));
let outcome = backend.rebuild(&ctx.device, &ctx.queue);
assert!(matches!(
outcome,
Err(GpuRebuildError::OwnerInvariantBroken { .. })
));
}
#[test]
fn write_between_release_and_rebuild_is_a_noop_and_not_ready() {
let Some(ctx) = gpu_ctx() else {
eprintln!("screen-frame-bus gpu_loss: no wgpu adapter, skipping");
return;
};
let mut backend = WgpuStagingBackend::new(&ctx.device, WgpuStagingConfig::new(64));
backend.release();
backend.write(|buf| buf.fill(0xAA));
assert!(
!<WgpuStagingBackend as StagingBackend>::is_ready(&backend),
"write before rebuild must not flip ready",
);
let observed = backend.read(|buf| buf.len());
assert_eq!(observed, 0, "read before rebuild must observe empty mirror");
}
#[test]
fn surface_pair_with_wgpu_backend_round_trips_cpu_mirror() {
let Some(ctx) = gpu_ctx() else {
eprintln!("screen-frame-bus gpu_loss: no wgpu adapter, skipping");
return;
};
let cfg = WgpuStagingConfig::new(64);
let a = WgpuStagingBackend::new(&ctx.device, cfg);
let b = WgpuStagingBackend::new(&ctx.device, cfg);
assert_eq!(
STAGING_PAIR_LEN, 2,
"OBS staging pair is exactly two surfaces"
);
let mut pair: StagingSurfacePair<WgpuStagingBackend> = StagingSurfacePair::new([a, b]);
pair.submit(0, |buf| {
buf[0] = 0xDE;
buf[1] = 0xAD;
})
.expect("submit zero must succeed");
pair.submit(1, |buf| {
buf[0] = 0xBE;
buf[1] = 0xEF;
})
.expect("submit one must succeed");
let first = pair
.try_map(0, |buf| (buf[0], buf[1]))
.expect("map zero ready");
assert_eq!(first, (0xDE, 0xAD));
let second = pair
.try_map(1, |buf| (buf[0], buf[1]))
.expect("map one ready");
assert_eq!(second, (0xBE, 0xEF));
}
#[test]
fn registry_holds_mixed_packers_and_staging_backends() {
let Some(ctx) = gpu_ctx() else {
eprintln!("screen-frame-bus gpu_loss: no wgpu adapter, skipping");
return;
};
let registry = GpuLossRegistry::new();
let mut guards = Vec::new();
for i in 0..4u32 {
let bytes = 32u64 << (i % 4);
let backend = Box::new(WgpuStagingBackend::new(
&ctx.device,
WgpuStagingConfig::new(bytes),
));
guards.push(registry.register(backend));
}
let report = registry.handle_device_lost(&ctx.device, &ctx.queue);
assert_eq!(report.released_count, 4);
assert_eq!(report.rebuilt_count, 4);
assert_eq!(report.failed_count, 0);
for outcome in &report.outcomes {
assert!(matches!(outcome, RebuildOutcome::Rebuilt { .. }));
}
drop(guards);
}
#[test]
fn cpu_backend_still_works_alongside_wgpu_backend() {
let cpu = CpuStagingBackend::new(64);
assert!(<CpuStagingBackend as StagingBackend>::is_idle(&cpu));
assert!(<CpuStagingBackend as StagingBackend>::is_ready(&cpu));
}
#[test]
fn buffer_handle_disappears_after_release_and_reappears_after_rebuild() {
let Some(ctx) = gpu_ctx() else {
eprintln!("screen-frame-bus gpu_loss: no wgpu adapter, skipping");
return;
};
let mut backend = WgpuStagingBackend::new(&ctx.device, WgpuStagingConfig::new(128));
assert!(backend.buffer().is_some(), "buffer present after build");
backend.release();
assert!(backend.buffer().is_none(), "buffer absent after release");
backend
.rebuild(&ctx.device, &ctx.queue)
.expect("rebuild must succeed");
assert!(backend.buffer().is_some(), "buffer present after rebuild");
}
}
File diff suppressed because it is too large Load Diff