6ea036766a
Fourth de-coupling for the host crate carve (plan §W6.1 leaf). gpu.rs (inventory, selection preference, active-session accounting — deps only pf-host-config + pf-paths, no subsystem refs) moves to a new pf-gpu leaf so pf-encode/pf-capture/pf-vdisplay can consult the selected GPU without an orchestrator edge. ~50 crate::gpu:: sites repoint to pf_gpu::; the ~30 pub(crate) items become pub (crate API). assign_ids gets a macOS-only allow(dead_code) (used only by the Linux/Windows enumerate arms). Verified: Linux (home-worker-5) clippy -p pf-gpu -p punktfunk-host --all-targets -D warnings + pf-gpu tests (12 pass); Windows (192.168.1.158) clippy --features nvenc,amf-qsv --all-targets green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
237 lines
8.9 KiB
Rust
237 lines
8.9 KiB
Rust
//! GPU-tagged management endpoints: inventory + automatic/preferred selection. Split out of the
|
|
//! `mgmt` facade (plan §W5).
|
|
|
|
use super::shared::*;
|
|
|
|
/// One hardware GPU on the host (software/WARP adapters are never listed).
|
|
#[derive(Serialize, ToSchema)]
|
|
pub(crate) struct ApiGpu {
|
|
/// Stable identifier (`vendorid-deviceid-occurrence`, hex PCI ids) — pass to `setGpuPreference`.
|
|
/// Stable across reboots and driver updates, unlike an adapter index or LUID.
|
|
#[schema(example = "10de-2c05-0")]
|
|
id: String,
|
|
/// Adapter/marketing name.
|
|
#[schema(example = "NVIDIA GeForce RTX 5070 Ti")]
|
|
name: String,
|
|
/// `nvidia` | `amd` | `intel` | `other`.
|
|
vendor: String,
|
|
/// Dedicated VRAM in MiB (0 where the platform doesn't expose it).
|
|
vram_mb: u64,
|
|
}
|
|
|
|
/// The GPU the **next** session's pipeline will be created on, and why. (A preference change
|
|
/// applies to the next session; a running session keeps the GPU it opened on.)
|
|
#[derive(Serialize, ToSchema)]
|
|
pub(crate) struct ApiSelectedGpu {
|
|
id: String,
|
|
name: String,
|
|
/// `nvidia` | `amd` | `intel` | `other`.
|
|
vendor: String,
|
|
/// Why this GPU was selected: `preference` (the manual choice), `env`
|
|
/// (`PUNKTFUNK_RENDER_ADAPTER`), `auto` (max dedicated VRAM / platform default), or
|
|
/// `preference_missing` (a manual choice is set but that GPU is absent — auto-selected
|
|
/// instead so the host keeps streaming).
|
|
source: String,
|
|
}
|
|
|
|
/// The GPU live sessions are encoding on right now.
|
|
#[derive(Serialize, ToSchema)]
|
|
pub(crate) struct ApiActiveGpu {
|
|
/// Stable id matching an entry of `gpus` (empty for the CPU/software encoder).
|
|
id: String,
|
|
name: String,
|
|
/// `nvidia` | `amd` | `intel` | `other`.
|
|
vendor: String,
|
|
/// The encode backend in use (`nvenc` | `amf` | `qsv` | `vaapi` | `software`).
|
|
backend: String,
|
|
/// Number of live encode sessions on it.
|
|
sessions: u32,
|
|
}
|
|
|
|
/// Full GPU-selection state for the console: inventory, the persisted preference, what the next
|
|
/// session will use, and what is in use right now.
|
|
#[derive(Serialize, ToSchema)]
|
|
pub(crate) struct GpuState {
|
|
/// The host's hardware GPUs.
|
|
gpus: Vec<ApiGpu>,
|
|
/// `auto` or `manual`.
|
|
mode: String,
|
|
/// The manually preferred GPU's stable id, when one is stored (kept while `mode` is `auto` so
|
|
/// a console can offer returning to it). May reference a GPU that is currently absent.
|
|
preferred_id: Option<String>,
|
|
/// The stored name of the preferred GPU (a usable label even when it is absent).
|
|
preferred_name: Option<String>,
|
|
/// Whether the preferred GPU is currently present.
|
|
preferred_available: bool,
|
|
/// `PUNKTFUNK_RENDER_ADAPTER` (the host.env pin), when set — it applies while `mode` is
|
|
/// `auto`; a manual preference overrides it.
|
|
env_override: Option<String>,
|
|
/// The GPU the next session will use.
|
|
selected: Option<ApiSelectedGpu>,
|
|
/// The GPU live sessions use right now (absent while nothing is streaming).
|
|
active: Option<ApiActiveGpu>,
|
|
}
|
|
|
|
/// Request body for `setGpuPreference`.
|
|
#[derive(Deserialize, ToSchema)]
|
|
pub(crate) struct SetGpuPreference {
|
|
/// `auto` (env pin, else max dedicated VRAM — the default) or `manual`.
|
|
#[schema(example = "manual")]
|
|
mode: String,
|
|
/// Required when `mode` is `manual`: the stable `id` of a currently listed GPU
|
|
/// (see `listGpus`).
|
|
#[schema(example = "10de-2c05-0")]
|
|
gpu_id: Option<String>,
|
|
}
|
|
|
|
/// Build the [`GpuState`] snapshot (shared by the GET and the PUT's response).
|
|
pub(crate) fn gpu_state() -> GpuState {
|
|
let gpus = pf_gpu::enumerate();
|
|
let pref = pf_gpu::prefs().get();
|
|
let (preferred_id, preferred_name, preferred_available) = match &pref.gpu {
|
|
Some(want) => {
|
|
let found = pf_gpu::find_preferred(&gpus, want);
|
|
let id = match found {
|
|
// Canonical: the present GPU's id (identity may have matched loosely).
|
|
Some(i) => gpus[i].id.clone(),
|
|
None => format!(
|
|
"{:04x}-{:04x}-{}",
|
|
want.vendor_id, want.device_id, want.occurrence
|
|
),
|
|
};
|
|
let name = match found {
|
|
Some(i) => gpus[i].name.clone(),
|
|
None => want.name.clone(),
|
|
};
|
|
(Some(id), Some(name), found.is_some())
|
|
}
|
|
None => (None, None, false),
|
|
};
|
|
let selected = pf_gpu::selected_gpu().map(|sel| ApiSelectedGpu {
|
|
vendor: sel.info.vendor_tag().into(),
|
|
id: sel.info.id,
|
|
name: sel.info.name,
|
|
source: sel.source.tag().into(),
|
|
});
|
|
let active = pf_gpu::active().and_then(|(g, sessions)| {
|
|
(sessions > 0).then(|| ApiActiveGpu {
|
|
vendor: pf_gpu::vendor_tag(g.vendor_id).into(),
|
|
id: g.id,
|
|
name: g.name,
|
|
backend: g.backend.into(),
|
|
sessions,
|
|
})
|
|
});
|
|
GpuState {
|
|
gpus: gpus
|
|
.into_iter()
|
|
.map(|g| ApiGpu {
|
|
vendor: g.vendor_tag().into(),
|
|
vram_mb: g.vram_bytes / (1024 * 1024),
|
|
id: g.id,
|
|
name: g.name,
|
|
})
|
|
.collect(),
|
|
mode: match pref.mode {
|
|
pf_gpu::GpuMode::Auto => "auto".into(),
|
|
pf_gpu::GpuMode::Manual => "manual".into(),
|
|
},
|
|
preferred_id,
|
|
preferred_name,
|
|
preferred_available,
|
|
env_override: pf_host_config::config()
|
|
.render_adapter
|
|
.clone()
|
|
.filter(|s| !s.is_empty()),
|
|
selected,
|
|
active,
|
|
}
|
|
}
|
|
|
|
/// GPU inventory and selection
|
|
///
|
|
/// Lists the host's hardware GPUs, the persisted auto/manual preference, the GPU the next session
|
|
/// will use (and why), and the GPU live sessions encode on right now.
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/gpus",
|
|
tag = "gpu",
|
|
operation_id = "listGpus",
|
|
responses(
|
|
(status = OK, description = "GPU inventory + selection state", body = GpuState),
|
|
(status = UNAUTHORIZED, description = "Missing or invalid bearer token", body = ApiError),
|
|
)
|
|
)]
|
|
pub(crate) async fn list_gpus() -> Json<GpuState> {
|
|
Json(gpu_state())
|
|
}
|
|
|
|
/// Set the GPU preference
|
|
///
|
|
/// `auto` restores automatic selection (`PUNKTFUNK_RENDER_ADAPTER` pin, else max dedicated VRAM);
|
|
/// `manual` pins capture + encode to the given GPU. Persisted across restarts; applies to the
|
|
/// **next** session (a running session keeps its GPU). If the preferred GPU is absent at session
|
|
/// start the host falls back to automatic selection rather than failing.
|
|
#[utoipa::path(
|
|
put,
|
|
path = "/gpus/preference",
|
|
tag = "gpu",
|
|
operation_id = "setGpuPreference",
|
|
request_body = SetGpuPreference,
|
|
responses(
|
|
(status = OK, description = "Preference stored; the new selection state", body = GpuState),
|
|
(status = BAD_REQUEST, description = "Unknown mode, or `gpu_id` missing / not a listed GPU", body = ApiError),
|
|
(status = INTERNAL_SERVER_ERROR, description = "Preference could not be persisted", body = ApiError),
|
|
(status = UNAUTHORIZED, description = "Missing or invalid bearer token", body = ApiError),
|
|
)
|
|
)]
|
|
pub(crate) async fn set_gpu_preference(ApiJson(req): ApiJson<SetGpuPreference>) -> Response {
|
|
let pref = match req.mode.to_ascii_lowercase().as_str() {
|
|
"auto" => {
|
|
// Keep the stored manual pick so the console can offer switching back to it.
|
|
let mut p = pf_gpu::prefs().get();
|
|
p.mode = pf_gpu::GpuMode::Auto;
|
|
p
|
|
}
|
|
"manual" => {
|
|
let Some(id) = req
|
|
.gpu_id
|
|
.as_deref()
|
|
.map(str::trim)
|
|
.filter(|s| !s.is_empty())
|
|
else {
|
|
return api_error(StatusCode::BAD_REQUEST, "mode `manual` requires `gpu_id`");
|
|
};
|
|
let Some(g) = pf_gpu::enumerate().into_iter().find(|g| g.id == id) else {
|
|
return api_error(
|
|
StatusCode::BAD_REQUEST,
|
|
"gpu_id does not match a present GPU (see GET /gpus)",
|
|
);
|
|
};
|
|
pf_gpu::GpuPreference {
|
|
mode: pf_gpu::GpuMode::Manual,
|
|
gpu: Some(pf_gpu::PreferredGpu {
|
|
vendor_id: g.vendor_id,
|
|
device_id: g.device_id,
|
|
occurrence: g.occurrence,
|
|
name: g.name,
|
|
}),
|
|
}
|
|
}
|
|
other => {
|
|
return api_error(
|
|
StatusCode::BAD_REQUEST,
|
|
&format!("unknown mode {other:?} — use `auto` or `manual`"),
|
|
)
|
|
}
|
|
};
|
|
if let Err(e) = pf_gpu::prefs().set(pref) {
|
|
return api_error(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
&format!("persist GPU preference: {e:#}"),
|
|
);
|
|
}
|
|
tracing::info!(mode = %req.mode, gpu_id = ?req.gpu_id, "management API: GPU preference updated");
|
|
Json(gpu_state()).into_response()
|
|
}
|