Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,42 @@ breadcrumbs localize any bring-up failure. Deviations to revisit: WPP/IFR
not wired (TraceLogging only); shell state is a process global keyed to
the single root-enumerated devnode.

Next: phase 4 (transport — swapchain worker → shared keyed-mutex texture
ring per plan step 3), then phase 5 (LuminalShine backend).
### Phase 4 (transport) — COMPLETE (2026-07-18, Windows box)

**Milestone verified: ring sequences advance while the desktop animates**
(2,108 frames published in a 30 s hold at 4K120, ephemeral identity, no
compositor stalls). The worker GPU-copies each acquired frame into named
keyed-mutex shared textures and publishes through the shared ring
section; `core::ring::RingPolicy` makes every slot decision. Ring state
lives in MonitorRt (sequences/generation survive reassignment); section
is created at plug (SDDL SYSTEM+Admins), textures lazily per frame-desc
(size change ⇒ generation bump).

Bring-up lessons (cost three compositor freezes to learn):
- `IddCxSwapChainReleaseAndAcquireBuffer` returns COM **E_PENDING
(0x8000000A)**, not STATUS_PENDING, for "no frame yet" — treating it as
fatal abandons the swapchain mid-activation and stalls the compositor
until the OS kills WUDFHost.
- On real acquire/publish failure: mark REBUILDING, retire textures,
**exit the worker** — never retry SetDevice on the same swapchain (the
OS drives recovery via unassign→assign; holding the dead swapchain
blocks modeset teardown).
- The OS unassigns+reassigns the swapchain ~10 ms after activation
(routine); first SetDevice often fails DXGI_ERROR_ACCESS_LOST —
harmless when the exit path is clean.
- Adapter caps: MaxDisplayPipelineRate=0 AND target-mode
RequiredBandwidth=0 (nonzero bandwidth vs zero budget makes every mode
unactivatable: Extend reverts, Scale/Resolution grayed).
- Windows remembers per-identity topology ("Disconnect this display"
sticks across sessions); vgd-probe --ephemeral mints a fresh identity.

Phase-5 notes: keyed-mutex protocol is key 0 pre-first-publish, key 1
after; readability travels in SlotMetadata.state (mutex only guards
pixels). Reader-side slot-state reconciliation (host CAS
PUBLISHED→READING→FREE, driver honoring shared state) lands with the
consumer. With no reader, drops ≈ published − slots (drop-oldest working
as specified). ETW: FrameLoopStart/RingTexturesCreated/
AcquireBufferFailedExit etc. under the provider GUID above.

Next: phase 5 (LuminalShine `luminalvgd` backend consuming the ring),
then WGC-RELIABILITY §7 alttab_stress port, cursor/gamma/HDR DDIs.
24 changes: 24 additions & 0 deletions crates/luminal-vgd-core/src/ring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ impl RingPolicy {
}
}

/// Writer could not complete the copy (bounded mutex acquire timed
/// out, device loss mid-write): revert the slot without publishing.
/// The frame is dropped and counted; any frame the acquire overwrote
/// was already counted by `writer_acquire`.
pub fn writer_abort(&mut self, index: usize) {
debug_assert_eq!(self.slots[index], Slot::Writing);
self.slots[index] = Slot::Free;
self.frames_dropped += 1;
}

/// Writer finished copying: publish the slot. Returns the frame's
/// sequence number (monotonic, gap-free on the writer side).
pub fn publish(&mut self, index: usize) -> u64 {
Expand Down Expand Up @@ -223,6 +233,20 @@ mod tests {
assert_eq!(r.frames_dropped, 1);
}

#[test]
fn writer_abort_reverts_and_counts_without_a_sequence() {
let mut r = RingPolicy::new(2);
let w = r.writer_acquire().unwrap();
r.writer_abort(w.index);
assert_eq!(r.slot(w.index), Slot::Free);
assert_eq!(r.frames_dropped, 1);
assert_eq!(r.frames_published, 0);
// The next publish still starts the sequence at 1 — aborts never
// consume sequence numbers, so readers see no phantom gap.
let w = r.writer_acquire().unwrap();
assert_eq!(r.publish(w.index), 1);
}

#[test]
fn rebuild_bumps_generation_but_sequences_continue() {
let mut r = RingPolicy::new(3);
Expand Down
4 changes: 4 additions & 0 deletions crates/luminal-vgd-driver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ windows = { version = "0.61", features = [
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D11",
"Win32_System_Threading",
"Win32_System_Memory",
"Win32_System_Performance",
"Win32_Security",
"Win32_Security_Authorization",
], optional = true }

[build-dependencies]
Expand Down
12 changes: 10 additions & 2 deletions crates/luminal-vgd-driver/src/shell/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,17 @@ pub fn apply_effects(effects: Vec<Effect>) {
connector_index,
modes,
adapter_luid,
ring_slots: _,
ring_slots,
edid,
} => monitors::plug(session_id, display_id, connector_index, modes, adapter_luid, edid),
} => monitors::plug(
session_id,
display_id,
connector_index,
modes,
adapter_luid,
ring_slots,
edid,
),
Effect::UnplugMonitor { session_id } => monitors::unplug(session_id),
Effect::PersistState(blob) => unsafe { write_persisted(&blob) },
}
Expand Down
5 changes: 5 additions & 0 deletions crates/luminal-vgd-driver/src/shell/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod control;
mod dxgi;
mod entry;
mod monitors;
mod ring;
mod swapchain;

// Private to `shell` but visible to all submodules (descendants see
Expand Down Expand Up @@ -67,6 +68,10 @@ pub(crate) struct MonitorRt {
pub edid: Box<[u8; 256]>,
pub modes: Vec<Mode>,
pub worker: Option<swapchain::Worker>,
/// The transport ring (section + policy + textures). Lives here, not
/// in the worker, so sequences and the generation persist across
/// swap-chain reassignments; the active worker drives it exclusively.
pub ring: std::sync::Arc<Mutex<swapchain::FrameRing>>,
}

pub(crate) struct Shell {
Expand Down
27 changes: 22 additions & 5 deletions crates/luminal-vgd-driver/src/shell/monitors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub fn plug(
connector_index: u32,
modes: Vec<Mode>,
_adapter_luid: u64,
ring_slots: u32,
edid: Box<[u8; 256]>,
) {
let shell = Shell::get();
Expand Down Expand Up @@ -70,13 +71,19 @@ pub fn plug(
}
let monitor = out_args.MonitorObject;

// The ring section exists from plug time (state ACTIVE, no frames
// yet) so the host can map it as soon as CREATE_MONITOR replies.
let ring = std::sync::Arc::new(std::sync::Mutex::new(
super::swapchain::FrameRing::new(session_id, ring_slots),
));
shell.monitors.lock().unwrap().insert(
session_id,
MonitorRt {
monitor: OsHandle(monitor.cast()),
edid,
modes,
worker: None,
ring,
},
);

Expand All @@ -96,7 +103,8 @@ pub fn plug(
}
}

/// Unplug: stop the frame worker (bounded), then IddCxMonitorDeparture.
/// Unplug: stop the frame worker (bounded), mark the ring DEAD so the
/// host unmaps, then IddCxMonitorDeparture.
pub fn unplug(session_id: u64) {
let shell = Shell::get();
let Some(mut rt) = shell.monitors.lock().unwrap().remove(&session_id) else {
Expand All @@ -105,6 +113,11 @@ pub fn unplug(session_id: u64) {
if let Some(worker) = rt.worker.take() {
worker.stop();
}
if let Ok(ring) = rt.ring.lock() {
if let Some(section) = &ring.section {
section.set_state(luminal_driver_proto::ring_state::DEAD);
}
}
unsafe {
let status = bindings::monitor_departure(rt.monitor.0.cast());
tracelogging::write_event!(
Expand Down Expand Up @@ -231,8 +244,10 @@ pub unsafe extern "C" fn evt_query_target_modes(
for (slot, mode) in slots.iter_mut().zip(modes.iter()) {
slot.Size = size_of::<ffi::IDDCX_TARGET_MODE>() as u32;
slot.TargetVideoSignalInfo.targetVideoSignalInfo = signal_info(mode, 1);
slot.RequiredBandwidth =
(mode.width as u64) * (mode.height as u64) * (mode.refresh_millihz as u64) / 1000;
// Zero, matching MaxDisplayPipelineRate = 0: bandwidth management
// unused. A nonzero requirement against a zero adapter budget makes
// every mode unactivatable (Extend reverts, Scale/Res grayed).
slot.RequiredBandwidth = 0;
}
out.TargetModeBufferOutputCount = fill as u32;
STATUS_SUCCESS
Expand Down Expand Up @@ -324,8 +339,10 @@ pub unsafe extern "C" fn evt_query_target_modes2(
for (slot, mode) in slots.iter_mut().zip(modes.iter()) {
slot.Size = size_of::<ffi::IDDCX_TARGET_MODE2>() as u32;
slot.TargetVideoSignalInfo.targetVideoSignalInfo = signal_info(mode, 1);
slot.RequiredBandwidth =
(mode.width as u64) * (mode.height as u64) * (mode.refresh_millihz as u64) / 1000;
// Zero, matching MaxDisplayPipelineRate = 0: bandwidth management
// unused. A nonzero requirement against a zero adapter budget makes
// every mode unactivatable (Extend reverts, Scale/Res grayed).
slot.RequiredBandwidth = 0;
slot.BitsPerComponent = wire_bpc_sdr8();
}
out.TargetModeBufferOutputCount = fill as u32;
Expand Down
Loading