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
+522
View File
@@ -0,0 +1,522 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bit-set"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
[[package]]
name = "bitflags"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]]
name = "bytemuck"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
dependencies = [
"bytemuck_derive",
]
[[package]]
name = "bytemuck_derive"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "codespan-reporting"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
dependencies = [
"unicode-width",
]
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "document-features"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
dependencies = [
"litrs",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "fluxer_gpu_rebuild"
version = "0.0.0"
dependencies = [
"parking_lot",
"wgpu",
]
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"num-traits",
"zerocopy",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "hexf-parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "litrs"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
[[package]]
name = "naga"
version = "29.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd91265cc2454558f659b3b4b9640f0ddb8cc6521277f166b8a8c181c898079"
dependencies = [
"arrayvec",
"bit-set",
"bitflags",
"cfg-if",
"cfg_aliases",
"codespan-reporting",
"half",
"hashbrown 0.16.1",
"hexf-parse",
"indexmap",
"libm",
"log",
"num-traits",
"once_cell",
"rustc-hash",
"thiserror",
"unicode-ident",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "portable-atomic"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
name = "portable-atomic-util"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
dependencies = [
"portable-atomic",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "profiling"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "raw-window-handle"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "renderdoc-sys"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "wgpu"
version = "29.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb3feacc458f7bee8bc1737149b42b6c731aa461039a4264a67bb6681646b250"
dependencies = [
"arrayvec",
"bitflags",
"bytemuck",
"cfg-if",
"cfg_aliases",
"document-features",
"hashbrown 0.16.1",
"log",
"portable-atomic",
"profiling",
"raw-window-handle",
"smallvec",
"static_assertions",
"wgpu-core",
"wgpu-hal",
"wgpu-types",
]
[[package]]
name = "wgpu-core"
version = "29.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02da3ad1b568337f25513b317870960ef87073ea0945502e44b864b67a8c77b7"
dependencies = [
"arrayvec",
"bit-set",
"bit-vec",
"bitflags",
"bytemuck",
"cfg_aliases",
"document-features",
"hashbrown 0.16.1",
"indexmap",
"log",
"naga",
"once_cell",
"parking_lot",
"portable-atomic",
"profiling",
"raw-window-handle",
"rustc-hash",
"smallvec",
"thiserror",
"wgpu-core-deps-windows-linux-android",
"wgpu-hal",
"wgpu-naga-bridge",
"wgpu-types",
]
[[package]]
name = "wgpu-core-deps-windows-linux-android"
version = "29.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb01076d0aa08b0ba9bd741e178b5cc440f5abe99d9581323a4c8b5d1a1916"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-hal"
version = "29.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31f8e1a9e7a8512f276f7c62e018c7fa8d60954303fed2e5750114332049193f"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libloading",
"log",
"naga",
"portable-atomic",
"portable-atomic-util",
"raw-window-handle",
"renderdoc-sys",
"thiserror",
"wgpu-naga-bridge",
"wgpu-types",
]
[[package]]
name = "wgpu-naga-bridge"
version = "29.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59c654c483f058800972c3645e95388a7eca31bf9fe1933bc20e036588a0be02"
dependencies = [
"naga",
"wgpu-types",
]
[[package]]
name = "wgpu-types"
version = "29.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9bcc31518a0e9735aefebedb5f7a9ef3ed1c42549c9f4c882fa9060ceaac639"
dependencies = [
"bitflags",
"bytemuck",
"log",
"raw-window-handle",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "zerocopy"
version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
@@ -0,0 +1,23 @@
[package]
name = "fluxer_gpu_rebuild"
version = "0.0.0"
edition = "2024"
license = "AGPL-3.0-or-later"
publish = false
[workspace]
resolver = "2"
[lib]
crate-type = ["rlib"]
[features]
default = []
wgpu = ["dep:wgpu"]
[dependencies]
parking_lot = "0.12"
wgpu = { version = "29", optional = true, default-features = false }
[dev-dependencies]
parking_lot = "0.12"
@@ -0,0 +1,677 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![deny(clippy::too_many_lines)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
#![deny(warnings)]
use parking_lot::Mutex;
use std::sync::Arc;
pub const MAX_REGISTERED_OWNERS: usize = 1024;
#[cfg(feature = "wgpu")]
pub type GpuDevice = wgpu::Device;
#[cfg(feature = "wgpu")]
pub type GpuQueue = wgpu::Queue;
#[cfg(not(feature = "wgpu"))]
#[derive(Debug)]
pub struct GpuDevice {
pub id: u64,
}
#[cfg(not(feature = "wgpu"))]
#[derive(Debug)]
pub struct GpuQueue {
pub id: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GpuRebuildError {
DeviceRejected { reason: &'static str },
ResourceCreateFailed { reason: &'static str },
OwnerInvariantBroken { reason: &'static str },
Other { code: u32 },
}
impl std::fmt::Display for GpuRebuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DeviceRejected { reason } => write!(f, "device rejected: {reason}"),
Self::ResourceCreateFailed { reason } => write!(f, "resource create failed: {reason}"),
Self::OwnerInvariantBroken { reason } => write!(f, "owner invariant broken: {reason}"),
Self::Other { code } => write!(f, "other rebuild error code={code}"),
}
}
}
impl std::error::Error for GpuRebuildError {}
pub type OwnerId = u64;
pub trait GpuLossCallback: Send {
fn release(&mut self);
fn rebuild(&mut self, device: &GpuDevice, queue: &GpuQueue) -> Result<(), GpuRebuildError>;
fn is_ready(&self) -> bool;
fn debug_label(&self) -> &'static str {
"<unlabelled-gpu-owner>"
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RebuildOutcome {
Rebuilt {
owner_id: OwnerId,
label: &'static str,
},
Failed {
owner_id: OwnerId,
label: &'static str,
error: GpuRebuildError,
},
Vacant {
owner_id: OwnerId,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RebuildReport {
pub released_count: u32,
pub rebuilt_count: u32,
pub failed_count: u32,
pub vacant_count: u32,
pub outcomes: Vec<RebuildOutcome>,
}
impl RebuildReport {
pub fn is_total_success(&self) -> bool {
self.failed_count == 0 && self.rebuilt_count > 0
}
pub fn is_empty_pass(&self) -> bool {
self.released_count == 0
&& self.rebuilt_count == 0
&& self.failed_count == 0
&& self.vacant_count == 0
}
}
struct Slot {
owner_id: OwnerId,
callback: Option<Box<dyn GpuLossCallback>>,
}
struct RegistryInner {
slots: Vec<Slot>,
next_owner_id: OwnerId,
registration_order_monotonic_floor: OwnerId,
}
impl RegistryInner {
fn new() -> Self {
Self {
slots: Vec::with_capacity(MAX_REGISTERED_OWNERS),
next_owner_id: 1,
registration_order_monotonic_floor: 0,
}
}
fn deregister(&mut self, owner_id: OwnerId) {
assert!(owner_id > 0, "owner_id must be positive");
assert!(
owner_id < self.next_owner_id,
"owner_id must come from a real registration"
);
let before = self.slots.len();
self.slots.retain(|slot| slot.owner_id != owner_id);
let after = self.slots.len();
assert!(after <= before, "deregister must not grow slots");
assert!(
after >= before.saturating_sub(1),
"deregister removes at most one slot"
);
}
}
pub struct GpuLossRegistry {
inner: Arc<Mutex<RegistryInner>>,
}
impl GpuLossRegistry {
pub fn new() -> Self {
let inner = Arc::new(Mutex::new(RegistryInner::new()));
let registry = Self { inner };
assert_eq!(registry.len(), 0, "fresh registry must be empty");
assert!(registry.is_empty(), "fresh registry must report empty");
registry
}
pub fn len(&self) -> usize {
let guard = self.inner.lock();
guard.slots.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn register(&self, owner: Box<dyn GpuLossCallback>) -> RegistrationGuard {
let mut guard = self.inner.lock();
assert!(
guard.slots.len() < MAX_REGISTERED_OWNERS,
"registry exceeds MAX_REGISTERED_OWNERS",
);
let id = guard.next_owner_id;
assert!(
id > guard.registration_order_monotonic_floor,
"ids must be monotonic"
);
guard.registration_order_monotonic_floor = id;
guard.next_owner_id = id.checked_add(1).unwrap_or(OwnerId::MAX);
guard.slots.push(Slot {
owner_id: id,
callback: Some(owner),
});
drop(guard);
RegistrationGuard {
owner_id: id,
registry: Arc::clone(&self.inner),
}
}
pub fn handle_device_lost(
&self,
new_device: &GpuDevice,
new_queue: &GpuQueue,
) -> RebuildReport {
let mut guard = self.inner.lock();
assert!(
guard.slots.len() <= MAX_REGISTERED_OWNERS,
"slots within cap before walk",
);
let released_count = release_in_reverse_order(&mut guard.slots);
let outcomes = rebuild_in_forward_order(&mut guard.slots, new_device, new_queue);
let report = summarize_outcomes(outcomes, released_count);
assert_pair_total_accounting(&guard.slots, &report);
assert_pair_post_rebuild_ready(&guard.slots, &report);
report
}
}
impl Default for GpuLossRegistry {
fn default() -> Self {
Self::new()
}
}
pub struct RegistrationGuard {
owner_id: OwnerId,
registry: Arc<Mutex<RegistryInner>>,
}
impl RegistrationGuard {
pub fn owner_id(&self) -> OwnerId {
assert!(self.owner_id > 0, "guard must hold a valid id");
assert!(
Arc::strong_count(&self.registry) >= 1,
"registry must outlive guard"
);
self.owner_id
}
}
impl Drop for RegistrationGuard {
fn drop(&mut self) {
let owner_id = self.owner_id;
if owner_id == 0 {
return;
}
let mut guard = self.registry.lock();
guard.deregister(owner_id);
}
}
fn release_in_reverse_order(slots: &mut [Slot]) -> u32 {
let total = slots.len();
assert!(
total <= MAX_REGISTERED_OWNERS,
"slots within cap on release"
);
let mut released: u32 = 0;
for slot in slots.iter_mut().rev() {
if let Some(callback) = slot.callback.as_mut() {
callback.release();
released = released.saturating_add(1);
}
}
assert!(released as usize <= total, "released cannot exceed total");
released
}
fn rebuild_in_forward_order(
slots: &mut [Slot],
device: &GpuDevice,
queue: &GpuQueue,
) -> Vec<RebuildOutcome> {
let total = slots.len();
assert!(
total <= MAX_REGISTERED_OWNERS,
"slots within cap on rebuild"
);
let mut outcomes: Vec<RebuildOutcome> = Vec::with_capacity(total);
for slot in slots.iter_mut() {
let owner_id = slot.owner_id;
assert!(owner_id > 0, "rebuild slot must have non-zero id");
let outcome = rebuild_single_slot(slot, device, queue);
outcomes.push(outcome);
}
assert_eq!(outcomes.len(), total, "one outcome per slot");
outcomes
}
fn rebuild_single_slot(slot: &mut Slot, device: &GpuDevice, queue: &GpuQueue) -> RebuildOutcome {
let owner_id = slot.owner_id;
let callback = match slot.callback.as_mut() {
Some(cb) => cb,
None => return RebuildOutcome::Vacant { owner_id },
};
let label = callback.debug_label();
match callback.rebuild(device, queue) {
Ok(()) => RebuildOutcome::Rebuilt { owner_id, label },
Err(error) => RebuildOutcome::Failed {
owner_id,
label,
error,
},
}
}
fn summarize_outcomes(outcomes: Vec<RebuildOutcome>, released_count: u32) -> RebuildReport {
let mut rebuilt_count: u32 = 0;
let mut failed_count: u32 = 0;
let mut vacant_count: u32 = 0;
for outcome in outcomes.iter() {
match outcome {
RebuildOutcome::Rebuilt { .. } => rebuilt_count = rebuilt_count.saturating_add(1),
RebuildOutcome::Failed { .. } => failed_count = failed_count.saturating_add(1),
RebuildOutcome::Vacant { .. } => vacant_count = vacant_count.saturating_add(1),
}
}
let report = RebuildReport {
released_count,
rebuilt_count,
failed_count,
vacant_count,
outcomes,
};
assert_eq!(
report.outcomes.len() as u32,
report.rebuilt_count + report.failed_count + report.vacant_count,
"outcome totals must match",
);
report
}
fn assert_pair_total_accounting(slots: &[Slot], report: &RebuildReport) {
let total = slots.len() as u32;
assert_eq!(
total,
report.rebuilt_count + report.failed_count + report.vacant_count,
"report covers every slot",
);
let mut last_id: OwnerId = 0;
for slot in slots.iter() {
assert!(
slot.owner_id > last_id,
"registration ids must be monotonic"
);
last_id = slot.owner_id;
}
}
fn assert_pair_post_rebuild_ready(slots: &[Slot], report: &RebuildReport) {
assert_eq!(
slots.len(),
report.outcomes.len(),
"slot count must match report"
);
for (slot, outcome) in slots.iter().zip(report.outcomes.iter()) {
match outcome {
RebuildOutcome::Rebuilt { owner_id, .. } => {
assert_eq!(*owner_id, slot.owner_id, "owner id alignment");
if let Some(cb) = slot.callback.as_ref() {
assert!(cb.is_ready(), "rebuilt owner must report ready");
}
}
RebuildOutcome::Failed { owner_id, .. } => {
assert_eq!(*owner_id, slot.owner_id, "failed owner id alignment");
}
RebuildOutcome::Vacant { owner_id } => {
assert_eq!(*owner_id, slot.owner_id, "vacant owner id alignment");
assert!(slot.callback.is_none(), "vacant slot must have no callback");
}
}
}
}
#[cfg(all(test, not(feature = "wgpu")))]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
use std::thread;
fn make_device_queue() -> (GpuDevice, GpuQueue) {
(GpuDevice { id: 42 }, GpuQueue { id: 42 })
}
#[derive(Default)]
struct Counters {
release_calls: AtomicU32,
rebuild_calls: AtomicU32,
release_seq: AtomicU32,
rebuild_seq: AtomicU32,
}
struct MockOwner {
counters: Arc<Counters>,
ready: bool,
fail_rebuild: bool,
release_order: Arc<Mutex<Vec<u32>>>,
rebuild_order: Arc<Mutex<Vec<u32>>>,
slot_marker: u32,
label: &'static str,
}
impl GpuLossCallback for MockOwner {
fn release(&mut self) {
self.ready = false;
let n = self.counters.release_calls.fetch_add(1, Ordering::SeqCst);
self.counters.release_seq.store(n + 1, Ordering::SeqCst);
let mut order = self.release_order.lock();
order.push(self.slot_marker);
}
fn rebuild(
&mut self,
_device: &GpuDevice,
_queue: &GpuQueue,
) -> Result<(), GpuRebuildError> {
let n = self.counters.rebuild_calls.fetch_add(1, Ordering::SeqCst);
self.counters.rebuild_seq.store(n + 1, Ordering::SeqCst);
let mut order = self.rebuild_order.lock();
order.push(self.slot_marker);
if self.fail_rebuild {
return Err(GpuRebuildError::ResourceCreateFailed {
reason: "mock fail",
});
}
self.ready = true;
Ok(())
}
fn is_ready(&self) -> bool {
self.ready
}
fn debug_label(&self) -> &'static str {
self.label
}
}
fn fresh_counters() -> Arc<Counters> {
Arc::new(Counters::default())
}
fn make_owner(
marker: u32,
counters: Arc<Counters>,
release_order: Arc<Mutex<Vec<u32>>>,
rebuild_order: Arc<Mutex<Vec<u32>>>,
) -> Box<MockOwner> {
Box::new(MockOwner {
counters,
ready: true,
fail_rebuild: false,
release_order,
rebuild_order,
slot_marker: marker,
label: "mock",
})
}
#[test]
fn five_owners_release_lifo_rebuild_fifo() {
let registry = GpuLossRegistry::new();
let counters = fresh_counters();
let release_order = Arc::new(Mutex::new(Vec::new()));
let rebuild_order = Arc::new(Mutex::new(Vec::new()));
let mut guards = Vec::new();
for i in 0..5u32 {
let owner = make_owner(
i,
Arc::clone(&counters),
Arc::clone(&release_order),
Arc::clone(&rebuild_order),
);
guards.push(registry.register(owner));
}
assert_eq!(registry.len(), 5);
let (device, queue) = make_device_queue();
let report = registry.handle_device_lost(&device, &queue);
assert_eq!(report.released_count, 5);
assert_eq!(report.rebuilt_count, 5);
assert_eq!(report.failed_count, 0);
assert_eq!(report.vacant_count, 0);
let rel = release_order.lock().clone();
assert_eq!(rel, vec![4, 3, 2, 1, 0]);
let reb = rebuild_order.lock().clone();
assert_eq!(reb, vec![0, 1, 2, 3, 4]);
drop(guards);
}
#[test]
fn owner_rebuild_failure_does_not_abort_other_owners() {
let registry = GpuLossRegistry::new();
let counters = fresh_counters();
let release_order = Arc::new(Mutex::new(Vec::new()));
let rebuild_order = Arc::new(Mutex::new(Vec::new()));
let mut guards = Vec::new();
for i in 0..4u32 {
let mut owner = make_owner(
i,
Arc::clone(&counters),
Arc::clone(&release_order),
Arc::clone(&rebuild_order),
);
if i == 1 {
owner.fail_rebuild = true;
}
guards.push(registry.register(owner));
}
let (device, queue) = make_device_queue();
let report = registry.handle_device_lost(&device, &queue);
assert_eq!(report.released_count, 4);
assert_eq!(report.rebuilt_count, 3);
assert_eq!(report.failed_count, 1);
let failed = report
.outcomes
.iter()
.filter(|o| matches!(o, RebuildOutcome::Failed { .. }))
.count();
assert_eq!(failed, 1);
drop(guards);
}
#[test]
fn registration_guard_drop_deregisters() {
let registry = GpuLossRegistry::new();
let counters = fresh_counters();
let release_order = Arc::new(Mutex::new(Vec::new()));
let rebuild_order = Arc::new(Mutex::new(Vec::new()));
let owner = make_owner(
7,
Arc::clone(&counters),
Arc::clone(&release_order),
Arc::clone(&rebuild_order),
);
let guard = registry.register(owner);
assert_eq!(registry.len(), 1);
drop(guard);
assert_eq!(registry.len(), 0);
assert!(registry.is_empty());
}
#[test]
fn empty_registry_device_loss_is_noop() {
let registry = GpuLossRegistry::new();
let (device, queue) = make_device_queue();
let report = registry.handle_device_lost(&device, &queue);
assert!(report.is_empty_pass());
assert!(!report.is_total_success());
assert_eq!(report.outcomes.len(), 0);
}
#[test]
fn concurrent_registration_is_safe() {
let registry = Arc::new(GpuLossRegistry::new());
let counters = fresh_counters();
let release_order = Arc::new(Mutex::new(Vec::new()));
let rebuild_order = Arc::new(Mutex::new(Vec::new()));
let mut threads = Vec::new();
let guards_collector: Arc<Mutex<Vec<RegistrationGuard>>> = Arc::new(Mutex::new(Vec::new()));
for i in 0..16u32 {
let registry = Arc::clone(&registry);
let counters = Arc::clone(&counters);
let release_order = Arc::clone(&release_order);
let rebuild_order = Arc::clone(&rebuild_order);
let guards_collector = Arc::clone(&guards_collector);
threads.push(thread::spawn(move || {
let owner = make_owner(i, counters, release_order, rebuild_order);
let guard = registry.register(owner);
let mut store = guards_collector.lock();
store.push(guard);
}));
}
for t in threads {
assert!(t.join().is_ok());
}
assert_eq!(registry.len(), 16);
}
#[test]
fn pair_asserted_invariants_under_stress() {
let registry = GpuLossRegistry::new();
let counters = fresh_counters();
let release_order = Arc::new(Mutex::new(Vec::new()));
let rebuild_order = Arc::new(Mutex::new(Vec::new()));
let mut guards = Vec::new();
for i in 0..64u32 {
let mut owner = make_owner(
i,
Arc::clone(&counters),
Arc::clone(&release_order),
Arc::clone(&rebuild_order),
);
if i % 7 == 0 {
owner.fail_rebuild = true;
}
guards.push(registry.register(owner));
}
let (device, queue) = make_device_queue();
for _ in 0..5 {
release_order.lock().clear();
rebuild_order.lock().clear();
let _report = registry.handle_device_lost(&device, &queue);
}
let rel = release_order.lock().clone();
let reb = rebuild_order.lock().clone();
assert_eq!(rel.len(), 64);
assert_eq!(reb.len(), 64);
for i in 0..64u32 {
assert_eq!(rel[i as usize], 63 - i);
assert_eq!(reb[i as usize], i);
}
drop(guards);
assert!(registry.is_empty());
}
#[test]
fn mock_callback_counters_are_deterministic() {
let registry = GpuLossRegistry::new();
let counters = fresh_counters();
let release_order = Arc::new(Mutex::new(Vec::new()));
let rebuild_order = Arc::new(Mutex::new(Vec::new()));
let guard = registry.register(make_owner(
1,
Arc::clone(&counters),
Arc::clone(&release_order),
Arc::clone(&rebuild_order),
));
let (device, queue) = make_device_queue();
for _ in 0..3 {
registry.handle_device_lost(&device, &queue);
}
assert_eq!(counters.release_calls.load(Ordering::SeqCst), 3);
assert_eq!(counters.rebuild_calls.load(Ordering::SeqCst), 3);
drop(guard);
}
#[test]
fn deregister_middle_owner_preserves_order() {
let registry = GpuLossRegistry::new();
let counters = fresh_counters();
let release_order = Arc::new(Mutex::new(Vec::new()));
let rebuild_order = Arc::new(Mutex::new(Vec::new()));
let g0 = registry.register(make_owner(
0,
Arc::clone(&counters),
Arc::clone(&release_order),
Arc::clone(&rebuild_order),
));
let g1 = registry.register(make_owner(
1,
Arc::clone(&counters),
Arc::clone(&release_order),
Arc::clone(&rebuild_order),
));
let g2 = registry.register(make_owner(
2,
Arc::clone(&counters),
Arc::clone(&release_order),
Arc::clone(&rebuild_order),
));
assert_eq!(registry.len(), 3);
drop(g1);
assert_eq!(registry.len(), 2);
let (device, queue) = make_device_queue();
let report = registry.handle_device_lost(&device, &queue);
assert_eq!(report.rebuilt_count, 2);
let rel = release_order.lock().clone();
assert_eq!(rel, vec![2, 0]);
let reb = rebuild_order.lock().clone();
assert_eq!(reb, vec![0, 2]);
drop(g0);
drop(g2);
assert!(registry.is_empty());
}
#[test]
fn rebuild_report_total_success_flag() {
let registry = GpuLossRegistry::new();
let counters = fresh_counters();
let release_order = Arc::new(Mutex::new(Vec::new()));
let rebuild_order = Arc::new(Mutex::new(Vec::new()));
let guard = registry.register(make_owner(
0,
Arc::clone(&counters),
Arc::clone(&release_order),
Arc::clone(&rebuild_order),
));
let (device, queue) = make_device_queue();
let report = registry.handle_device_lost(&device, &queue);
assert!(report.is_total_success());
assert!(!report.is_empty_pass());
drop(guard);
}
}