fix(linux/vdisplay): KWin virtual outputs stream above 60 Hz via sacrificial-birth renegotiation

KWin's ScreenCastStream builds its PipeWire format offer — including the
maxFramerate cap it actively throttles delivery to — ONCE at stream
creation, when the virtual output sits at its hardcoded birth 60 Hz. A
kscreen custom-mode change afterwards updates the OUTPUT (readback says
240) but never the offer, so every consumer negotiates max=60 and the
stream delivers 60 (pw-dump-verified on KWin 6.6.4). The only path that
rebuilds the offer is the stream's own resize handling: a source SIZE
change while recording re-runs buildFormats — picking up the output's
current refresh — and renegotiates the live stream.

So above 60 Hz, birth the output at a sacrificial height (+16), then
install + select the real WxH@hz custom mode: the first frame recorded
after the consumer connects triggers KWin's resize path, which renegotiates
the live stream to WxH@hz. The capturer holds (requeues) buffers until the
negotiated size matches — new expect_exact_dims plumbing VirtualOutput →
registry (fresh creates only) → open_virtual_output → a self-disarming
gate in the process callback — bounded by a 3 s deadline that accepts the
producer's dims rather than wedging the session into the first-frame
retry loop. set_custom_refresh reads back the full active mode; a size
reject (pre-6.6 KWin) recreates plain at the real size @60.

Every kscreen operation now addresses the output by its NUMERIC id,
resolved by matching the managed name-prefix AND the just-created birth
size (newest id wins): a mode-switch supersede reuses the per-slot output
NAME (deliberately, for KWin's per-name config persistence) while the
superseded sibling is still alive, and name addressing hit the FIRST
match = the OLD output — on-glass that resized the live session's display
out from under it (wrong-res/black), read the old output back as 'mode
applied', and set the old output primary while the replacement starved at
its birth mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 23:47:53 +02:00
co-authored by Claude Fable 5
parent 09113c9899
commit af87549052
9 changed files with 339 additions and 89 deletions
@@ -70,6 +70,12 @@ pub struct VirtualOutput {
/// Linux-only (the keep-alive pool is Linux).
#[cfg(target_os = "linux")]
pub pool_gen: Option<u64>,
/// The backend created the output at a SACRIFICIAL mode and the producer will renegotiate the
/// live stream to `preferred_mode`'s dims (KWin's screencast only rebuilds its format offer —
/// and thus its refresh cap — on a size change while recording; see kwin.rs `create`). The
/// capturer must hold frames until that renegotiation lands. Linux-only.
#[cfg(target_os = "linux")]
pub expect_exact_dims: bool,
}
impl VirtualOutput {
@@ -93,6 +99,8 @@ impl VirtualOutput {
reused_gen: None,
#[cfg(target_os = "linux")]
pool_gen: None,
#[cfg(target_os = "linux")]
expect_exact_dims: false,
}
}
}
@@ -253,6 +253,7 @@ impl VirtualDisplay for GamescopeDisplay {
ownership: DisplayOwnership::External,
reused_gen: None,
pool_gen: None,
expect_exact_dims: false,
});
}
check_gamescope_version(); // diagnostic only — warns on known-deadlock-prone versions
@@ -409,6 +410,7 @@ fn managed_output(node_id: u32, mode: Mode) -> VirtualOutput {
ownership: DisplayOwnership::SessionManaged,
reused_gen: None,
pool_gen: None,
expect_exact_dims: false,
}
}
@@ -199,6 +199,7 @@ impl VirtualDisplay for HyprlandDisplay {
ownership: DisplayOwnership::Owned,
reused_gen: None,
pool_gen: None,
expect_exact_dims: false,
})
}
}
+219 -84
View File
@@ -82,8 +82,10 @@ pub struct KwinDisplay {
/// `None` for shared/anonymous) — reported to the registry via [`last_identity_slot`] so it can key
/// the group arrangement + `/display/state` slot to the same id this backend named the output with.
last_slot: Option<u32>,
/// The base output name the last `create` used (`punktfunk` / `punktfunk-<id>`) — so
/// [`apply_position`](VirtualDisplay::apply_position) can address the KWin output `Virtual-<name>`.
/// The RESOLVED kscreen address of the last `create`'s output — the numeric kscreen output id
/// when [`resolve_kscreen_addr`] found it, else the `Virtual-<name>` fallback — so
/// [`apply_position`](VirtualDisplay::apply_position) addresses OUR output even while a
/// superseded same-name sibling is still alive.
last_name: Option<String>,
/// The topology-restore action the last `create` prepared (re-enable the outputs an `exclusive`
/// topology disabled), pending pickup by the registry via [`take_topology_restore`] — so the
@@ -127,11 +129,13 @@ impl VirtualDisplay for KwinDisplay {
}
fn apply_position(&mut self, x: i32, y: i32) {
let Some(name) = self.last_name.clone() else {
// `last_name` holds the RESOLVED kscreen address (numeric output id, or the
// `Virtual-<name>` fallback) — never re-derive from the name: during a supersede two
// outputs share it and the command would hit the old one (see `create`).
let Some(output) = self.last_name.clone() else {
return;
};
let output = format!("Virtual-{name}");
// kscreen-doctor position syntax: `output.<name>.position.<x>,<y>`.
// kscreen-doctor position syntax: `output.<name-or-id>.position.<x>,<y>`.
let ok = std::process::Command::new("kscreen-doctor")
.arg(format!("output.{output}.position.{x},{y}"))
.status()
@@ -161,32 +165,86 @@ impl VirtualDisplay for KwinDisplay {
None => VOUT_NAME.to_string(),
};
self.last_name = Some(name.clone()); // for apply_position (registry-driven §6.2 layout)
let (setup_tx, setup_rx) = std::sync::mpsc::channel::<Result<u32, String>>();
let stop = Arc::new(AtomicBool::new(false));
let stop_thread = stop.clone();
let (width, height) = (mode.width, mode.height);
let name_thread = name.clone();
thread::Builder::new()
.name("punktfunk-kwin-vout".into())
.spawn(move || virtual_output_thread(width, height, name_thread, setup_tx, stop_thread))
.context("spawn KWin virtual-output thread")?;
let node_id = match setup_rx.recv_timeout(Duration::from_secs(20)) {
Ok(Ok(v)) => v,
Ok(Err(e)) => bail!("KWin virtual output failed: {e}"),
Err(_) => bail!("timed out creating the KWin virtual output"),
let spawn_vout = |w: u32, h: u32| -> Result<(u32, Arc<AtomicBool>)> {
let (setup_tx, setup_rx) = std::sync::mpsc::channel::<Result<u32, String>>();
let stop = Arc::new(AtomicBool::new(false));
let stop_thread = stop.clone();
let name_thread = name.clone();
thread::Builder::new()
.name("punktfunk-kwin-vout".into())
.spawn(move || virtual_output_thread(w, h, name_thread, setup_tx, stop_thread))
.context("spawn KWin virtual-output thread")?;
match setup_rx.recv_timeout(Duration::from_secs(20)) {
Ok(Ok(v)) => Ok((v, stop)),
Ok(Err(e)) => bail!("KWin virtual output failed: {e}"),
Err(_) => bail!("timed out creating the KWin virtual output"),
}
};
tracing::info!(node_id, width, height, "KWin virtual output ready");
// KWin creates virtual outputs at a hardcoded 60 Hz and `stream_virtual_output` has no
// refresh argument, so above 60 Hz we install + select a custom mode (supported on virtual
// outputs since KWin 6.6) before capture connects PipeWire, so the stream negotiates at the
// higher rate. First cut shells out to kscreen-doctor; the in-process
// kde_output_management_v2 client is a follow-up. `set_custom_refresh` reads back and
// returns what KWin *actually* achieved so the encoder paces to the real source rate (a
// rejected custom mode leaves the output at 60 Hz). At ≤60 Hz there's nothing to install —
// the source runs 60 Hz and the encoder downsamples — so carry the requested rate through.
let achieved_hz = if mode.refresh_hz > 60 {
set_custom_refresh(width, height, mode.refresh_hz, &name)
// KWin creates virtual outputs at a hardcoded 60 Hz, `stream_virtual_output` has no
// refresh argument — and its screencast stream builds its PipeWire format offer, INCLUDING
// the `maxFramerate` cap it actively throttles delivery to, ONCE at stream creation
// (screencaststream.cpp `buildFormats`, verified against the live offer with `pw-dump`:
// the offer stays `max=60` after a kscreen mode change, and consumers connecting later
// still negotiate against it). The ONLY path that rebuilds the offer is the stream's own
// resize handling: when the source's texture size changes while recording, KWin re-runs
// `buildFormats` — picking up the output's CURRENT refresh — and renegotiates the live
// stream via `pw_stream_update_params`. So above 60 Hz the output is born at a
// SACRIFICIAL height: installing + selecting the real `WxH@hz` custom mode (supported on
// virtual outputs since KWin 6.6) then changes the SIZE, and the first buffers recorded
// after the consumer connects trigger KWin's resize → a renegotiation to `WxH@hz`. The
// capturer holds frames until that lands (`expect_exact_dims`), so the pipeline never
// builds against the birth mode. First cut shells out to kscreen-doctor; the in-process
// kde_output_management_v2 client is a follow-up. `set_custom_refresh` reads back what
// KWin *actually* gave so the encoder paces to the real source rate. At ≤60 Hz there's
// nothing to install — the output is born at the real size and 60 Hz is the offer anyway.
let want_high = mode.refresh_hz > 60;
let birth_h = if want_high { height + 16 } else { height };
let (mut node_id, mut stop) = spawn_vout(width, birth_h)?;
tracing::info!(node_id, width, height, birth_h, "KWin virtual output ready");
// ⚠️ ADDRESS BY NUMERIC KSCREEN ID, NEVER BY NAME: a supersede (mode switch) creates the
// replacement output — SAME per-slot name, deliberately, for KWin's per-name config
// persistence — while the superseded sibling is still alive (create-before-drop). Every
// name-addressed kscreen command then hits the FIRST match = the OLD output: on-glass this
// resized the LIVE session's display out from under it (wrong-res/black), read back the
// OLD output as "custom mode applied", made the OLD output primary, and positioned it —
// while the new output never left its birth mode and the capturer's dims gate starved.
// Resolve OUR output's kscreen id once (match: managed-prefix name AND current mode ==
// the just-created birth size; newest id wins), and use it for every kscreen operation.
let mut addr = resolve_kscreen_addr(&name, width, birth_h);
self.last_name = Some(addr.clone()); // for apply_position (registry-driven §6.2 layout)
let mut expect_exact_dims = false;
let achieved_hz = if want_high {
let (achieved, size_applied) =
set_custom_refresh(width, height, mode.refresh_hz, &addr);
if size_applied {
// Real mode selected: the recording stream will renegotiate to it (see above).
expect_exact_dims = true;
achieved
} else {
// Custom-mode install/select rejected (pre-6.6 KWin / stale kscreen-doctor): the
// output is STUCK at the sacrificial birth size — unusable. Recreate plain at the
// real size (the pre-sacrifice behavior: correct size, KWin's native 60 Hz).
tracing::warn!(
"KWin rejected the custom mode — recreating the virtual output at the real \
size (60 Hz ceiling on this KWin)"
);
stop.store(true, Ordering::Relaxed);
// Let KWin retire the doomed output before re-using its name.
std::thread::sleep(Duration::from_millis(300));
let (nid, st) = spawn_vout(width, height)?;
node_id = nid;
stop = st;
addr = resolve_kscreen_addr(&name, width, height);
self.last_name = Some(addr.clone());
tracing::info!(
node_id,
width,
height,
"KWin virtual output ready (fallback)"
);
60
}
} else {
mode.refresh_hz
};
@@ -197,9 +255,9 @@ impl VirtualDisplay for KwinDisplay {
// bootstrap output. Read from the policy (replacing the PUNKTFUNK_KWIN_VIRTUAL_PRIMARY boolean).
use crate::policy::Topology;
let disabled = match crate::effective_topology() {
Topology::Exclusive => apply_virtual_primary(&name),
Topology::Exclusive => apply_virtual_primary(&addr),
Topology::Primary => {
apply_virtual_primary_only(&name);
apply_virtual_primary_only(&addr);
Vec::new() // nothing disabled → nothing to restore
}
Topology::Extend | Topology::Auto => Vec::new(),
@@ -215,11 +273,13 @@ impl VirtualDisplay for KwinDisplay {
});
// Layout position (§6.2) is applied by the registry via `apply_position` right after create
// (it owns the display group, so it computes auto-row / manual placement over the whole group).
Ok(VirtualOutput::owned(
let mut out = VirtualOutput::owned(
node_id,
Some((mode.width, mode.height, achieved_hz)),
Box::new(StopGuard { stop }),
))
);
out.expect_exact_dims = expect_exact_dims;
Ok(out)
}
}
@@ -257,14 +317,88 @@ fn reenable_outputs(outputs: &[(String, String)]) {
tracing::info!(reenabled = ?outputs, "KWin: restored the physical/bootstrap outputs at their captured modes (group empty)");
}
/// Best-effort: raise the just-created virtual output's refresh above KWin's default 60 Hz by
/// installing + selecting a custom mode via `kscreen-doctor` (the output is `Virtual-<VOUT_NAME>`,
/// refresh given in mHz), then **read back the active mode** and return the refresh KWin actually
/// gave us. The apply command can report success yet leave the output at 60 Hz (mode rejected),
/// and a silent rate mismatch surfaces downstream as judder / duplicated frames — so the caller
/// paces the encoder to the *achieved* rate, not the requested one.
fn set_custom_refresh(width: u32, height: u32, hz: u32, name: &str) -> u32 {
let output = format!("Virtual-{name}");
/// Resolve the kscreen address of the virtual output the host JUST created: the managed-prefix
/// name alone is ambiguous during a supersede (the replacement deliberately reuses the per-slot
/// name while the superseded sibling is still alive), so match on the birth mode's size too —
/// only the just-created output sits at the sacrificial `(w, h)` — and prefer the HIGHEST output
/// id (the newest) if several match. Returns the numeric id as a string (kscreen-doctor accepts
/// `output.<id>.…`), falling back to the ambiguous `Virtual-<name>` if the output hasn't reached
/// kscreen's model yet after a few tries (single-output sessions are unambiguous anyway).
fn resolve_kscreen_addr(name: &str, w: u32, h: u32) -> String {
let fallback = format!("Virtual-{name}");
for attempt in 0..3 {
if attempt > 0 {
std::thread::sleep(Duration::from_millis(150));
}
let Some(doc) = kscreen_json() else { continue };
let Some(outputs) = doc.get("outputs").and_then(|o| o.as_array()) else {
continue;
};
let best = outputs
.iter()
.filter(|o| {
o.get("name")
.and_then(|n| n.as_str())
.is_some_and(|n| n.starts_with(&fallback))
&& output_active_size(o) == Some((w, h))
})
.filter_map(|o| o.get("id").and_then(|i| i.as_u64()))
.max();
if let Some(id) = best {
tracing::info!(id, name, w, h, "KWin: resolved the new output's kscreen id");
return id.to_string();
}
}
tracing::warn!(
name,
w,
h,
"KWin: could not resolve the new output's kscreen id — falling back to name addressing \
(ambiguous during a mode-switch supersede)"
);
fallback
}
/// `kscreen-doctor -j` parsed, `None` on any failure.
fn kscreen_json() -> Option<serde_json::Value> {
let out = std::process::Command::new("kscreen-doctor")
.arg("-j")
.output()
.ok()?;
serde_json::from_slice(&out.stdout).ok()
}
/// The `(width, height)` of an output's CURRENT mode from its `kscreen-doctor -j` entry.
fn output_active_size(o: &serde_json::Value) -> Option<(u32, u32)> {
let as_id = |v: &serde_json::Value| -> Option<String> {
v.as_str()
.map(|s| s.to_string())
.or_else(|| v.as_u64().map(|n| n.to_string()))
};
let current = o.get("currentModeId").and_then(as_id)?;
let mode = o
.get("modes")?
.as_array()?
.iter()
.find(|m| m.get("id").and_then(as_id).as_deref() == Some(current.as_str()))?;
let size = mode.get("size")?;
Some((
size.get("width").and_then(|v| v.as_u64())? as u32,
size.get("height").and_then(|v| v.as_u64())? as u32,
))
}
/// Best-effort: install + select the `width`x`height`@`hz` custom mode on the just-created
/// virtual output via `kscreen-doctor` (`output` is the RESOLVED kscreen address — numeric id or
/// name, see [`resolve_kscreen_addr`] — refresh given in mHz), then **read back the active mode**
/// and return `(achieved_hz, size_applied)`. The apply command can report success yet leave the
/// output on its old mode (rejected), and a silent rate mismatch surfaces downstream as judder /
/// duplicated frames — so the caller paces the encoder to the *achieved* rate, not the requested
/// one. `size_applied` tells the sacrificial-birth caller (see `create`) whether the SIZE half of
/// the mode actually landed — that, not the refresh, is what triggers KWin's stream
/// renegotiation.
fn set_custom_refresh(width: u32, height: u32, hz: u32, output: &str) -> (u32, bool) {
let output = output.to_string();
let mhz = hz.saturating_mul(1000);
let run = |arg: String| {
std::process::Command::new("kscreen-doctor")
@@ -278,26 +412,29 @@ fn set_custom_refresh(width: u32, height: u32, hz: u32, name: &str) -> u32 {
"output.{output}.addCustomMode.{width}.{height}.{mhz}.full"
));
let applied = run(format!("output.{output}.mode.{width}x{height}@{hz}"));
match read_active_refresh(&output) {
Some(achieved) if achieved >= hz => {
tracing::info!(
output,
requested = hz,
achieved,
"KWin virtual output: custom refresh applied"
);
achieved
}
Some(achieved) => {
tracing::warn!(
output,
requested = hz,
achieved,
applied,
"KWin virtual output refresh below requested — pacing the encoder to the achieved \
rate (custom-mode install rejected? is kscreen-doctor up to date?)"
);
achieved.max(1)
match read_active_mode(&output) {
Some((w, h, achieved)) => {
let size_applied = (w, h) == (width, height);
if achieved >= hz && size_applied {
tracing::info!(
output,
requested = hz,
achieved,
"KWin virtual output: custom refresh applied"
);
} else {
tracing::warn!(
output,
requested = hz,
achieved,
active_w = w,
active_h = h,
applied,
"KWin virtual output mode below requested — pacing the encoder to the \
achieved rate (custom-mode install rejected? is kscreen-doctor up to date?)"
);
}
(achieved.max(1), size_applied)
}
None => {
tracing::warn!(
@@ -307,38 +444,37 @@ fn set_custom_refresh(width: u32, height: u32, hz: u32, name: &str) -> u32 {
"could not read back KWin virtual output refresh — assuming 60 Hz (is \
kscreen-doctor installed?)"
);
60
(60, false)
}
}
}
/// Read the active refresh (Hz, rounded) of `output` from `kscreen-doctor -j`. `None` if the
/// tool, the output, or its current mode can't be found. Mode/output ids come through as either
/// JSON strings or numbers depending on the KWin version, so both are accepted.
fn read_active_refresh(output: &str) -> Option<u32> {
let out = std::process::Command::new("kscreen-doctor")
.arg("-j")
.output()
.ok()?;
let doc: serde_json::Value = serde_json::from_slice(&out.stdout).ok()?;
/// Read the active mode (`(width, height, refresh_hz)`, Hz rounded) of `output` — a RESOLVED
/// kscreen address (numeric id or name, see [`resolve_kscreen_addr`]) — from `kscreen-doctor -j`.
/// `None` if the tool, the output, or its current mode can't be found. Mode/output ids come
/// through as either JSON strings or numbers depending on the KWin version, so both are accepted.
fn read_active_mode(output: &str) -> Option<(u32, u32, u32)> {
let doc = kscreen_json()?;
let as_id = |v: &serde_json::Value| -> Option<String> {
v.as_str()
.map(|s| s.to_string())
.or_else(|| v.as_u64().map(|n| n.to_string()))
};
let o = doc
.get("outputs")?
.as_array()?
.iter()
.find(|o| o.get("name").and_then(|n| n.as_str()) == Some(output))?;
let o = doc.get("outputs")?.as_array()?.iter().find(|o| {
o.get("name").and_then(|n| n.as_str()) == Some(output)
|| o.get("id").and_then(as_id).as_deref() == Some(output)
})?;
let current = o.get("currentModeId").and_then(as_id)?;
let mode = o
.get("modes")?
.as_array()?
.iter()
.find(|m| m.get("id").and_then(as_id).as_deref() == Some(current.as_str()))?;
let size = mode.get("size")?;
let w = size.get("width").and_then(|v| v.as_u64())? as u32;
let h = size.get("height").and_then(|v| v.as_u64())? as u32;
let hz = mode.get("refreshRate").and_then(|r| r.as_f64())?;
Some(hz.round() as u32)
Some((w, h, hz.round() as u32))
}
/// The prefix EVERY managed KWin output shares — Stage 3 names them `punktfunk` / `punktfunk-<id>`,
@@ -377,7 +513,7 @@ fn output_current_mode_spec(o: &serde_json::Value) -> Option<String> {
/// bare re-enable drops a 120 Hz panel to KWin's default ~60 Hz).
/// **Group-aware (§6.1):** excludes the WHOLE managed family (the [`MANAGED_PREFIX`]), not just this
/// session's own output — so a 2nd `exclusive` session (with a distinct per-slot name) never disables
/// the 1st session's live output. Parsed from `kscreen-doctor -j` (same source as [`read_active_refresh`]).
/// the 1st session's live output. Parsed from `kscreen-doctor -j` (same source as [`read_active_mode`]).
fn other_enabled_outputs() -> Vec<(String, String)> {
let out = match std::process::Command::new("kscreen-doctor")
.arg("-j")
@@ -440,12 +576,12 @@ fn a_managed_output_is_primary() -> bool {
.unwrap_or(false)
}
/// Set `Virtual-punktfunk` primary and disable the bootstrap output(s) so the managed group becomes
/// the sole desktop (KWin re-homes plasmashell + windows onto it). Returns the disabled outputs for
/// Set our output primary and disable the bootstrap output(s) so the managed group becomes
/// the sole desktop (KWin re-homes plasmashell + windows onto it). `ours` is the RESOLVED kscreen
/// address (numeric id or name, see [`resolve_kscreen_addr`]). Returns the disabled outputs for
/// the keepalive to re-enable on teardown. Best-effort: on failure, streaming continues (just possibly
/// showing only the wallpaper) rather than failing the session.
fn apply_virtual_primary(name: &str) -> Vec<(String, String)> {
let ours = format!("Virtual-{name}");
fn apply_virtual_primary(ours: &str) -> Vec<(String, String)> {
let kscreen = |args: &[String]| {
std::process::Command::new("kscreen-doctor")
.args(args)
@@ -483,8 +619,7 @@ fn apply_virtual_primary(name: &str) -> Vec<(String, String)> {
/// **Primary** (Stage 2): make the streamed output the primary but KEEP the other outputs enabled
/// (don't disable the bootstrap/physical) — so the shell re-homes onto the streamed surface while a
/// physical screen stays usable. Nothing to restore on teardown (we disabled nothing).
fn apply_virtual_primary_only(name: &str) {
let ours = format!("Virtual-{name}");
fn apply_virtual_primary_only(ours: &str) {
let ok = std::process::Command::new("kscreen-doctor")
.arg(format!("output.{ours}.primary"))
.status()
@@ -136,6 +136,7 @@ impl VirtualDisplay for WlrootsDisplay {
ownership: DisplayOwnership::Owned,
reused_gen: None,
pool_gen: None,
expect_exact_dims: false,
})
}
}
+8 -1
View File
@@ -584,6 +584,11 @@ mod linux {
let node_id = real.node_id;
let preferred_mode = real.preferred_mode;
// Fresh creates only: the backend may have birthed the output at a sacrificial mode whose
// stream must renegotiate before frames count (KWin >60 Hz — see backend.rs). A REUSED kept
// display already renegotiated in its prior session (the producer's rebuilt offer persists
// across consumer reconnects), so the reuse path above correctly leaves the flag off.
let expect_exact_dims = real.expect_exact_dims;
// The backend's topology-restore action (KWin `exclusive` → re-enable the disabled physicals),
// lifted into the group so it runs once when the group's last member drops (§6.1), not at this
// session's teardown. `None` for non-exclusive / non-first / backends whose topology auto-reverts.
@@ -647,7 +652,9 @@ mod linux {
if (position.x, position.y) != (0, 0) {
vd.apply_position(position.x, position.y);
}
Ok(output_for(node_id, preferred_mode, gen, quit, false))
let mut out = output_for(node_id, preferred_mode, gen, quit, false);
out.expect_exact_dims = expect_exact_dims;
Ok(out)
}
/// The linger a releasing session actually gets. A deliberate quit (`force_immediate` — the