diff --git a/CLAUDE.md b/CLAUDE.md index d3fb708..99c08fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -206,3 +206,36 @@ Integration lessons (all host-side, none required driver changes): Next: tranche 3b — ring-consuming capture backend in LuminalShine (`display_vgd` platf::display_t), driver HDR10 caps, cursor/gamma DDIs, WGC-RELIABILITY §7 alttab_stress port. + +### Tranche 3b + HDR10 — COMPLETE (2026-07-20, Windows box) + +**Milestones verified the same day:** (1) LuminalShine consumes the +frame ring directly (`display_vgd_vram_t`: claim → keyed-mutex key 1 → +GPU copy → release; no WGC helper, latency parity ~5 ms) and (2) **HDR10 +end to end** — driver build 2 (caps 0x185) creates HDR monitors +(bit_depth=110 wire value), Windows engages advanced color off our +CTA-861.3 EDID block, the ring carries FP16 scRGB, and LuminalShine +encodes HEVC Main10 4:4:4 with HDR metadata. AV1 HDR 10-bit also works; +AV1 4:4:4 is an NVENC hardware gap on RTX 5080 (not a software item). + +HDR bring-up lessons (one wasted signing round each — check first): +- **IDDCX_ADAPTER_FLAGS_CAN_PROCESS_FP16 is a contract, not a flag.** + AdapterInitAsync fails STATUS_INVALID_PARAMETER unless the driver also + registers EvtIddCxMonitorSetGammaRamp (HDR 3x4 matrix; GammaSupport + must not be NONE) and acquires via IddCxSwapChainReleaseAndAcquire- + Buffer2 (METADATA2). See "Updates for IddCx 1.10" doc for the full + obligation list. ETW breadcrumbs localize this in one traced + pnputil /restart-device — no re-sign needed to diagnose. +- **Proto bit_depth wire values are 8/10/110/112** (HDR carries a + leading "1"); hdr=1 with bit_depth=10 is BAD_BIT_DEPTH (-4), and the + host log only shows "result=-4" — check dispatch err codes first. +- Ring textures follow the acquired frame's DXGI format; an + advanced-color toggle (BGRA8 ⇄ FP16) is a generation bump like a size + change. Host reader re-latches format automatically. +- Host-side stall detection must key off `latest_sequence` vs the last + delivered sequence, never cumulative publish counters — an idle + desktop is indistinguishable from a stall by counters alone. + +Next: cursor + gamma DDIs (hardware cursor ABI is in proto v0.3), +WGC-RELIABILITY §7 alttab_stress port, phase 7 packaging (installer, +strict control-device SDDL — release blocker, uninstaller). diff --git a/crates/luminal-vgd-driver/src/shell/bindings.rs b/crates/luminal-vgd-driver/src/shell/bindings.rs index 9244f27..52df50c 100644 --- a/crates/luminal-vgd-driver/src/shell/bindings.rs +++ b/crates/luminal-vgd-driver/src/shell/bindings.rs @@ -97,6 +97,23 @@ pub unsafe fn swapchain_release_and_acquire_buffer( ) } +/// v1.10 variant — mandatory for CAN_PROCESS_FP16 adapters (per-frame +/// IDDCX_METADATA2: HDR metadata, surface color space, SDR white level). +/// Safe to call unconditionally: IddMinimumVersionRequired = 10 means the +/// OS only loads us with a ≥1.10 function table. +pub unsafe fn swapchain_release_and_acquire_buffer2( + swapchain: IDDCX_SWAPCHAIN, + in_args: *mut IDARG_IN_RELEASEANDACQUIREBUFFER2, + out_args: *mut IDARG_OUT_RELEASEANDACQUIREBUFFER2, +) -> NTSTATUS { + iddcx_call!( + _IDDFUNCENUM_IddCxSwapChainReleaseAndAcquireBuffer2TableIndex as PFN_IDDCXSWAPCHAINRELEASEANDACQUIREBUFFER2, + swapchain, + in_args, + out_args + ) +} + pub unsafe fn swapchain_finished_processing_frame(swapchain: IDDCX_SWAPCHAIN) -> NTSTATUS { iddcx_call!( _IDDFUNCENUM_IddCxSwapChainFinishedProcessingFrameTableIndex as PFN_IDDCXSWAPCHAINFINISHEDPROCESSINGFRAME, diff --git a/crates/luminal-vgd-driver/src/shell/entry.rs b/crates/luminal-vgd-driver/src/shell/entry.rs index 5cd1603..4a9d58d 100644 --- a/crates/luminal-vgd-driver/src/shell/entry.rs +++ b/crates/luminal-vgd-driver/src/shell/entry.rs @@ -88,6 +88,9 @@ unsafe extern "C" fn evt_device_add( idd.EvtIddCxAdapterCommitModes2 = Some(monitors::evt_commit_modes2); idd.EvtIddCxMonitorSetDefaultHdrMetaData = Some(monitors::evt_set_default_hdr_metadata); idd.EvtIddCxMonitorQueryTargetModes2 = Some(monitors::evt_query_target_modes2); + // CAN_PROCESS_FP16 obligates the gamma-ramp DDI (HDR 3x4 matrix); + // an FP16 adapter without it fails AdapterInitAsync validation. + idd.EvtIddCxMonitorSetGammaRamp = Some(monitors::evt_monitor_set_gamma_ramp); let status = bindings::device_init_config(device_init, &idd); if status != STATUS_SUCCESS { @@ -183,6 +186,11 @@ unsafe extern "C" fn evt_d0_entry( let mut caps: ffi::IDDCX_ADAPTER_CAPS = zeroed(); caps.Size = size_of::() as u32; caps.MaxMonitorsSupported = luminal_driver_proto::DEFAULT_MAX_MONITORS; + // HDR10: the OS only offers advanced color on an indirect display when + // the adapter declares it can process FP16 desktop content; the frame + // pipeline is format-agnostic (ring textures follow the acquired + // frame's desc), so this is safe to declare unconditionally. + caps.Flags = ffi::IDDCX_ADAPTER_FLAGS_IDDCX_ADAPTER_FLAGS_CAN_PROCESS_FP16; // MaxDisplayPipelineRate stays 0 (IddSampleDriver convention); // u64::MAX fails IddCxAdapterInitAsync parameter validation. caps.EndPointDiagnostics.Size = size_of::() as u32; @@ -193,7 +201,10 @@ unsafe extern "C" fn evt_d0_entry( caps.EndPointDiagnostics.pEndPointManufacturerName = MANUFACTURER.as_ptr(); caps.EndPointDiagnostics.pHardwareVersion = addr_of_mut!(version); caps.EndPointDiagnostics.pFirmwareVersion = addr_of_mut!(version); - caps.EndPointDiagnostics.GammaSupport = ffi::IDDCX_FEATURE_IMPLEMENTATION_IDDCX_FEATURE_IMPLEMENTATION_NONE; + // SOFTWARE, matching the registered EvtIddCxMonitorSetGammaRamp: the + // HDR 3x4 matrix is acknowledged by the driver (conversion happens in + // the host encoder). NONE alongside CAN_PROCESS_FP16 is contradictory. + caps.EndPointDiagnostics.GammaSupport = ffi::IDDCX_FEATURE_IMPLEMENTATION_IDDCX_FEATURE_IMPLEMENTATION_SOFTWARE; let mut in_args: ffi::IDARG_IN_ADAPTER_INIT = zeroed(); in_args.WdfDevice = device; diff --git a/crates/luminal-vgd-driver/src/shell/mod.rs b/crates/luminal-vgd-driver/src/shell/mod.rs index 9fb7f23..aa034e4 100644 --- a/crates/luminal-vgd-driver/src/shell/mod.rs +++ b/crates/luminal-vgd-driver/src/shell/mod.rs @@ -29,15 +29,18 @@ use std::time::Instant; use crate::dispatch::{DeviceState, HandleCtx}; use luminal_vgd_core::modes::Mode; -/// Capabilities the phase-2 shell actually delivers. SDR only for now: -/// HDR, hardware cursor, and gamma caps are added when their DDIs land -/// (phases 2+/4 per docs/FEATURE-MATRIX.md). -pub(crate) const SHELL_CAPS: u32 = - luminal_driver_proto::caps::MULTI_MODE | luminal_driver_proto::caps::PERMANENT_POOL; +/// Capabilities the shell actually delivers. HDR10/SDR10 landed with the +/// HDR tranche (FP16-capable adapter, per-mode wire bit depth, format- +/// aware ring textures); hardware cursor and gamma caps are added when +/// their DDIs land (docs/FEATURE-MATRIX.md). +pub(crate) const SHELL_CAPS: u32 = luminal_driver_proto::caps::MULTI_MODE + | luminal_driver_proto::caps::PERMANENT_POOL + | luminal_driver_proto::caps::HDR10 + | luminal_driver_proto::caps::SDR10_BIT; /// Monotonic build stamp reported in HANDSHAKE/GET_STATUS (CI will stamp /// this properly in phase 7; hand-bumped during bring-up). -pub(crate) const DRIVER_BUILD: u32 = 1; +pub(crate) const DRIVER_BUILD: u32 = 2; /// NUL-terminated UTF-16 literal; size the array one past the text so the /// terminator survives. diff --git a/crates/luminal-vgd-driver/src/shell/monitors.rs b/crates/luminal-vgd-driver/src/shell/monitors.rs index a64b5a3..7d1bf91 100644 --- a/crates/luminal-vgd-driver/src/shell/monitors.rs +++ b/crates/luminal-vgd-driver/src/shell/monitors.rs @@ -269,10 +269,20 @@ pub unsafe extern "C" fn evt_commit_modes( // HDR paths get real implementations alongside caps::HDR10 later. // --------------------------------------------------------------------- -/// SDR-only wire format: 8-bit RGB, nothing else. -fn wire_bpc_sdr8() -> ffi::IDDCX_WIRE_BITS_PER_COMPONENT { +/// Wire format for one mode: RGB at the session's bit depth (8 for SDR-8, +/// 10 for SDR-10/HDR10, 12 for HDR12). The OS picks the highest depth the +/// whole path supports; HDR additionally needs the EDID's CTA-861.3 block +/// (core::edid) and the adapter's CAN_PROCESS_FP16 flag (shell::entry). +fn wire_bpc_for(mode: &Mode) -> ffi::IDDCX_WIRE_BITS_PER_COMPONENT { + use luminal_driver_proto::BitDepth; let mut bpc: ffi::IDDCX_WIRE_BITS_PER_COMPONENT = unsafe { zeroed() }; - bpc.Rgb = ffi::IDDCX_BITS_PER_COMPONENT_IDDCX_BITS_PER_COMPONENT_8; + bpc.Rgb = match mode.bit_depth { + BitDepth::Sdr8 => ffi::IDDCX_BITS_PER_COMPONENT_IDDCX_BITS_PER_COMPONENT_8, + BitDepth::Sdr10 | BitDepth::Hdr10 => { + ffi::IDDCX_BITS_PER_COMPONENT_IDDCX_BITS_PER_COMPONENT_10 + } + BitDepth::Hdr12 => ffi::IDDCX_BITS_PER_COMPONENT_IDDCX_BITS_PER_COMPONENT_12, + }; bpc } @@ -282,7 +292,11 @@ pub unsafe extern "C" fn evt_adapter_query_target_info( out_args: *mut ffi::IDARG_OUT_QUERYTARGET_INFO, ) -> NTSTATUS { let out = &mut *out_args; - out.TargetCaps = ffi::IDDCX_TARGET_CAPS_IDDCX_TARGET_CAPS_NONE; + // HDR10/advanced color needs the target to advertise the wide/high + // color-space pipeline; harmless for SDR-only monitors (the OS still + // gates on the per-monitor EDID + wire bit depth). + out.TargetCaps = ffi::IDDCX_TARGET_CAPS_IDDCX_TARGET_CAPS_HIGH_COLOR_SPACE + | ffi::IDDCX_TARGET_CAPS_IDDCX_TARGET_CAPS_WIDE_COLOR_SPACE; out.DitheringSupport = zeroed(); STATUS_SUCCESS } @@ -313,7 +327,7 @@ pub unsafe extern "C" fn evt_parse_monitor_description2( slot.Size = size_of::() as u32; slot.Origin = ffi::IDDCX_MONITOR_MODE_ORIGIN_IDDCX_MONITOR_MODE_ORIGIN_MONITORDESCRIPTOR; slot.MonitorVideoSignalInfo = signal_info(mode, 0); - slot.BitsPerComponent = wire_bpc_sdr8(); + slot.BitsPerComponent = wire_bpc_for(mode); } out.MonitorModeBufferOutputCount = fill as u32; STATUS_SUCCESS @@ -343,7 +357,7 @@ pub unsafe extern "C" fn evt_query_target_modes2( // 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(); + slot.BitsPerComponent = wire_bpc_for(mode); } out.TargetModeBufferOutputCount = fill as u32; STATUS_SUCCESS @@ -356,11 +370,47 @@ pub unsafe extern "C" fn evt_commit_modes2( STATUS_SUCCESS } +/// CAN_PROCESS_FP16 contract (IddCx 1.10): the OS provides the HDR 3x4 +/// color matrix here for adapters that advertise FP16 processing. Our +/// pipeline is pass-through — ring consumers receive the composed FP16 +/// scRGB desktop verbatim and the host encoder performs the colorspace +/// conversion — so the matrix is acknowledged and traced, not applied to +/// pixels. GammaSupport is declared SOFTWARE to match. +pub unsafe extern "C" fn evt_monitor_set_gamma_ramp( + monitor: ffi::IDDCX_MONITOR, + in_args: *const ffi::IDARG_IN_SET_GAMMARAMP, +) -> NTSTATUS { + let inp = &*in_args; + let ramp_type = inp.Type as u32; + tracelogging::write_event!( + PROVIDER, + "SetGammaRamp", + level(Informational), + u64("monitor", &(monitor as u64)), + u32("type", &ramp_type), + u32("size", &inp.GammaRampSizeInBytes) + ); + STATUS_SUCCESS +} + pub unsafe extern "C" fn evt_set_default_hdr_metadata( - _monitor: ffi::IDDCX_MONITOR, - _in_args: *const ffi::IDARG_IN_MONITOR_SET_DEFAULT_HDR_METADATA, + monitor: ffi::IDDCX_MONITOR, + in_args: *const ffi::IDARG_IN_MONITOR_SET_DEFAULT_HDR_METADATA, ) -> NTSTATUS { - // SDR-only shell: accept and ignore. Stored + used when caps::HDR10 - // and the phase-4 frame pipeline land. + // The OS pushes the desktop's effective HDR10 static metadata here when + // advanced color engages. The wire pixels already carry the composed + // desktop (FP16 scRGB), so nothing needs reprocessing — trace it for + // diagnostics; surfacing it to the host via GET_STATUS can come later + // if the encoder wants monitor-derived mastering data. + let inp = &*in_args; + let meta_type = inp.Type as u32; + tracelogging::write_event!( + PROVIDER, + "SetDefaultHdrMetaData", + level(Informational), + u64("monitor", &(monitor as u64)), + u32("type", &meta_type), + u32("size", &inp.Size) + ); STATUS_SUCCESS } diff --git a/crates/luminal-vgd-driver/src/shell/ring.rs b/crates/luminal-vgd-driver/src/shell/ring.rs index b27dccf..73499e9 100644 --- a/crates/luminal-vgd-driver/src/shell/ring.rs +++ b/crates/luminal-vgd-driver/src/shell/ring.rs @@ -25,7 +25,7 @@ use windows::Win32::Graphics::Direct3D11::{ D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX, D3D11_RESOURCE_MISC_SHARED_NTHANDLE, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, }; -use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC}; +use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT, DXGI_SAMPLE_DESC}; use windows::Win32::Graphics::Dxgi::{ IDXGIKeyedMutex, IDXGIResource1, DXGI_SHARED_RESOURCE_READ, DXGI_SHARED_RESOURCE_WRITE, }; @@ -298,8 +298,10 @@ pub fn acquire_mutex(mutex: &IDXGIKeyedMutex, key: u64, timeout_ms: u32) -> Acqu } /// Create the `count` named shared textures for (`session_id`, -/// `generation`) at the given dimensions. BGRA8 (the IddCx frame surface -/// format); keyed mutex starts at key 0 (driver-writable). +/// `generation`) at the given dimensions and format. The format follows +/// the acquired swapchain frame's desc — BGRA8 for SDR desktops, FP16 +/// (scRGB) or R10G10B10A2 once the OS composes the display in advanced +/// color; keyed mutex starts at key 0 (driver-writable). pub fn create_shared_textures( device: &ID3D11Device, session_id: u64, @@ -307,13 +309,14 @@ pub fn create_shared_textures( count: u32, width: u32, height: u32, + format: DXGI_FORMAT, ) -> windows::core::Result> { let desc = D3D11_TEXTURE2D_DESC { Width: width, Height: height, MipLevels: 1, ArraySize: 1, - Format: DXGI_FORMAT_B8G8R8A8_UNORM, + Format: format, SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 }, Usage: D3D11_USAGE_DEFAULT, BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32, diff --git a/crates/luminal-vgd-driver/src/shell/swapchain.rs b/crates/luminal-vgd-driver/src/shell/swapchain.rs index 553f422..e05abb5 100644 --- a/crates/luminal-vgd-driver/src/shell/swapchain.rs +++ b/crates/luminal-vgd-driver/src/shell/swapchain.rs @@ -55,6 +55,10 @@ pub(crate) struct FrameRing { textures: Vec, tex_width: u32, tex_height: u32, + /// DXGI_FORMAT raw value of the current textures; the OS switches the + /// swapchain surface format when advanced color toggles (BGRA8 ⇄ FP16), + /// which retires textures exactly like a size change. + tex_format: u32, /// Per-slot: has this slot ever been published this generation? A /// never-published slot's keyed mutex is still at its creation key /// (0, driver); after the first publish it lives at key 1 (host). @@ -88,6 +92,7 @@ impl FrameRing { textures: Vec::new(), tex_width: 0, tex_height: 0, + tex_format: 0, ever_published: vec![false; slots], assigned_before: false, } @@ -99,6 +104,7 @@ impl FrameRing { self.textures.clear(); self.tex_width = 0; self.tex_height = 0; + self.tex_format = 0; self.ever_published.iter_mut().for_each(|b| *b = false); let generation = self.policy.rebuild(); if let Some(s) = &self.section { @@ -299,9 +305,16 @@ fn frame_loop( last_heartbeat = Instant::now(); } - let mut out: ffi::IDARG_OUT_RELEASEANDACQUIREBUFFER = unsafe { zeroed() }; - let status = - unsafe { bindings::swapchain_release_and_acquire_buffer(swapchain.0.cast(), &mut out) }; + // Buffer2 is mandatory for CAN_PROCESS_FP16 adapters (IddCx 1.10): + // METADATA2 carries per-frame HDR metadata / surface color space / + // SDR white level alongside the same surface + QPC fields. + let mut in_args: ffi::IDARG_IN_RELEASEANDACQUIREBUFFER2 = unsafe { zeroed() }; + in_args.Size = size_of::() as u32; + let mut out: ffi::IDARG_OUT_RELEASEANDACQUIREBUFFER2 = unsafe { zeroed() }; + out.MetaData.Size = size_of::() as u32; + let status = unsafe { + bindings::swapchain_release_and_acquire_buffer2(swapchain.0.cast(), &mut in_args, &mut out) + }; if status == STATUS_PENDING || status == E_PENDING { unsafe { let _ = WaitForSingleObject(HANDLE(frame_event.0), 100); @@ -370,7 +383,7 @@ fn publish_frame( ring: &mut FrameRing, device: &ID3D11Device, context: &ID3D11DeviceContext, - meta: &ffi::IDDCX_METADATA, + meta: &ffi::IDDCX_METADATA2, ) -> windows::core::Result<()> { if ring.section.is_none() { return Ok(()); // transport disabled; drain-only @@ -387,9 +400,14 @@ fn publish_frame( unsafe { frame_tex.GetDesc(&mut desc) }; // Lazy texture (re)creation: first frame, or the committed mode - // changed size. A size change is a full generation bump so the host - // re-opens textures by name. - if ring.textures.is_empty() || desc.Width != ring.tex_width || desc.Height != ring.tex_height { + // changed size, or the surface format changed (advanced-color toggle + // switches BGRA8 ⇄ FP16). Any change is a full generation bump so the + // host re-opens textures by name and re-latches the format. + if ring.textures.is_empty() + || desc.Width != ring.tex_width + || desc.Height != ring.tex_height + || desc.Format.0 as u32 != ring.tex_format + { if !ring.textures.is_empty() { ring.retire_textures(); } @@ -401,9 +419,11 @@ fn publish_frame( slots, desc.Width, desc.Height, + desc.Format, )?; ring.tex_width = desc.Width; ring.tex_height = desc.Height; + ring.tex_format = desc.Format.0 as u32; tracelogging::write_event!( PROVIDER, "RingTexturesCreated", @@ -411,7 +431,8 @@ fn publish_frame( u64("session", &session_id), u32("generation", &ring.policy.generation), u32("width", &desc.Width), - u32("height", &desc.Height) + u32("height", &desc.Height), + u32("format", &(desc.Format.0 as u32)) ); } diff --git a/crates/luminal-vgd-ffi/include/luminal_vgd.h b/crates/luminal-vgd-ffi/include/luminal_vgd.h index e30b263..d609ded 100644 --- a/crates/luminal-vgd-ffi/include/luminal_vgd.h +++ b/crates/luminal-vgd-ffi/include/luminal_vgd.h @@ -20,6 +20,15 @@ */ #define VGD_ERR_IO -1000 +/** + * Capability bits of `VgdCaps.caps` the backend gates on (literal + * mirrors of proto `caps::*`; the asserts below keep them honest — + * cbindgen cannot evaluate cross-crate constants). + */ +#define VGD_CAP_HDR10 1 + +#define VGD_CAP_SDR10_BIT 4 + typedef struct VgdDeviceHandle VgdDeviceHandle; typedef struct VgdRingHandle VgdRingHandle; diff --git a/crates/luminal-vgd-ffi/src/lib.rs b/crates/luminal-vgd-ffi/src/lib.rs index 20d7fe7..6bd3ab4 100644 --- a/crates/luminal-vgd-ffi/src/lib.rs +++ b/crates/luminal-vgd-ffi/src/lib.rs @@ -31,6 +31,14 @@ use luminal_vgd_host::device::{RingView, VgdDevice}; /// negative proto `err::*` code from the driver. pub const VGD_ERR_IO: i32 = -1000; +/// Capability bits of `VgdCaps.caps` the backend gates on (literal +/// mirrors of proto `caps::*`; the asserts below keep them honest — +/// cbindgen cannot evaluate cross-crate constants). +pub const VGD_CAP_HDR10: u32 = 1; +pub const VGD_CAP_SDR10_BIT: u32 = 4; +const _: () = assert!(VGD_CAP_HDR10 == luminal_driver_proto::caps::HDR10); +const _: () = assert!(VGD_CAP_SDR10_BIT == luminal_driver_proto::caps::SDR10_BIT); + pub struct VgdDeviceHandle(VgdDevice); pub struct VgdRingHandle(RingView);