- 驱动开发
- 硬件开发
【免费下载链接】DsHidMini
Virtual HID Mini-user-mode-driver for Sony DualShock 3 Controllers
DsHidMini 是一套面向 Sony DualShock 3 手柄的虚拟 HID 用户态驱动方案,其 SDK 通过共享内存与驱动交换数据。本文以 DsInputReportMetrics 这一公开模型为切入点,完整讲解它承载的"输入报告到达指标"(每秒报告率与平均到达间隔)的语义、字节布局、驱动侧采集与发布流程、用户态读取方式以及在 ControlApp 中的实际展示。读完本文,你将能独立解读该结构体的每个字段,理解它如何在驱动DISPATCH_LEVEL的到达路径上被统计、如何以 seqlock 安全地跨越共享内存边界,以及如何在自家应用中接入并格式化这些指标。
一、指标是什么:宿主侧"到达节奏",而非单向延迟
DsInputReportMetrics是Nefarius.DsHidMini.IPC.Models.Public命名空间下的一个版本化、按插槽(per-slot)组织的输入报告到达指标结构。它的核心作用是让用户态程序每秒钟看到一次 DS3 手柄输入报告的到达频率与相邻报告之间的平均间隔,从而判断手柄链路(USB 或蓝牙)是否处于健康状态。
两个关键语义必须澄清:
- 间隔定义:
AverageIntervalUs度量的是宿主侧相邻成功报告之间的到达间距——即 USB 中断完成(interrupt completion)或蓝牙中断包先后到达主机的时间差,不是单向包延迟,也不是驱动到应用之间的传输时延。 - 0 值含义:当字段为
0时,表示在统计窗口内没有观察到足够数量的报告(例如设备刚连接、处于 D0 休眠、或链路没有数据到达),而非"间隔为零微秒"。
按 C# 侧的注释,ReportRateHz与AverageIntervalUs覆盖的是驱动最近完成的一个约 1 秒窗口("the driver's last completed ~1 s window"),这是驱动侧 1 Hz 发布节奏与用户态轮询节奏共同决定的。
二、数据结构:28 字节、Pack=1 的紧凑布局
结构体使用[StructLayout(LayoutKind.Sequential, Pack = 1)]声明,保证按 1 字节对齐、无填充字节,从而与驱动共享内存中的原生 C 结构严格一一对应,参见 InputReportMetrics.cs。
2.1 实例字段
| 字段 | C# 类型 | 偏移(推算) | 说明 |
|---|---|---|---|
SlotIndex | uint | 0 | 设备插槽索引(驱动侧写入Context->SlotIndex,从 1 开始;0 表示槽位已清空) |
SequenceNumber | int | 4 | seqlock 序号:奇数表示发布者正在写入,偶数且非零表示快照一致,用于无锁安全读取 |
Version | ushort | 8 | 结构体 ABI 版本,当前恒为CurrentVersion = 1 |
Reserved0 | ushort | 10 | 保留字段,发布时恒为 0,为未来扩展预留 |
ReportRateHz | uint | 12 | 最近 1 秒窗口内的平均报告到达率(Hz),0 表示无足够样本 |
AverageIntervalUs | uint | 16 | 窗口内相邻报告平均到达间隔(微秒),0 表示无足够样本 |
TimestampQpc | ulong | 20 | 本次发布时刻的 QueryPerformanceCounter(QPC)值,供用户态换算绝对时间戳 |
字段总长为 28 字节,与公开常量Size = 28一致。原始文档中字段按字母顺序列出的全部 7 个成员(AverageIntervalUs、ReportRateHz、Reserved0、SequenceNumber、SlotIndex、TimestampQpc、Version)均已在上表覆盖。
2.2 常量字段
public const ushort CurrentVersion = 1; // 当前 ABI 版本 public const int Size = 28; // 序列化后总字节数版本号是 IPC 兼容性的第一道防线:用户态程序在读取指标前应先读取驱动的InputReportMetricsVersionProperty设备属性确认 ABI 版本,再决定能否按本结构体解析(见下文第五节)。
三、驱动侧:到达统计、窗口计算与 1 Hz 发布
指标的全部采集与计算都发生在驱动内,核心实现在 driver/DsInputReportMetrics.c 与 driver/DsInputReportMetrics.h。
3.1 状态窗口:DS_INPUT_REPORT_METRICS_STATE
#define DS_INPUT_REPORT_METRICS_VERSION 1 #define DS_INPUT_REPORT_METRICS_PERIOD_MS 1000驱动维护的统计窗口包含:窗口起点 QPC(WindowStartQpc)、上一次到达 QPC(LastArrivalQpc)、窗口内间隔累加值(WindowIntervalSumQpc)、窗口内报告计数(WindowReportCount)与间隔计数(WindowIntervalCount),并由一把在 USB 连续读路径上以DISPATCH_LEVEL获取的自旋锁(WDFSPINLOCK Lock)保护。
3.2 到达记录:DsInputReportMetrics_NoteArrival
每当一个输入报告成功到达,驱动在报告处理路径上调用 DsInputReportMetrics_NoteArrival。调用点位于 driver/DsHidMiniDrv.c 与 driver/DsHidMiniDrv.c,分别覆盖 USB 与蓝牙两条到达路径。其逻辑为:
- 记录当前 QPC;
- 若存在上一次到达记录,则把两次到达的差值累加到
WindowIntervalSumQpc并递增WindowIntervalCount; - 更新
LastArrivalQpc,并递增WindowReportCount。
QueryPerformanceCounter(&now); WdfSpinLockAcquire(metrics->Lock); { if (metrics->LastArrivalQpc.QuadPart != 0) { metrics->WindowIntervalSumQpc += (UINT64)(now.QuadPart - metrics->LastArrivalQpc.QuadPart); metrics->WindowIntervalCount++; } metrics->LastArrivalQpc = now; metrics->WindowReportCount++; } WdfSpinLockRelease(metrics->Lock);这里"到达"既包含 USB 中断完成(interrupt completion),也包含蓝牙中断包,与文档中"host-side arrival spacing between successful USB interrupt completions or Bluetooth interrupt packets"的语义严格对应。
3.3 窗口计算与发布:DsInputReportMetrics_ComputeAndPublish
一个约 1 Hz 的发布器在PASSIVE_LEVEL下定时触发。注意一个 WDF 细节:UMDF 不接受被动级别的周期性定时器(periodic passive timers),因此驱动创建的是单次触发(one-shot)定时器,在回调中当Running标志仍置位时自我重新武装:
DsInputReportMetrics_ComputeAndPublish(context); WdfSpinLockAcquire(context->InputReportMetrics.Lock); { if (context->InputReportMetrics.Running) { WdfTimerStart(Timer, WDF_REL_TIMEOUT_IN_MS(DS_INPUT_REPORT_METRICS_PERIOD_MS)); } } WdfSpinLockRelease(context->InputReportMetrics.Lock);计算逻辑(DsInputReportMetrics_ComputeAndPublish):
- 报告率:
rateHz = reportCount * QPC频率 / 窗口流逝QPC,即把窗口内的报告数换算成一秒等效频率; - 平均间隔:
averageIntervalUs = intervalSumQpc * 1_000_000 / (QPC频率 * intervalCount),即窗口内所有相邻到达间隔的 QPC 均值再换算为微秒。
当窗口内reportCount == 0或intervalCount == 0时,对应指标保持 0,这与文档"0means no sufficient reports were observed"完全一致。发布成功后驱动会写入 ETW 跟踪日志(TraceVerbose:"Input report metrics: %u Hz, %u us avg interval (reports=%lu intervals=%lu)"),可用于调试时确认数据确实在流动。
四、与驱动的共享内存结构:ABI 必须保持同步
C# 侧结构体必须与驱动的 IPC_INPUT_REPORT_METRICS_MESSAGE 保持字节级一致,二者都被要求Pack=1:
#include <pshpack1.h> typedef struct _IPC_INPUT_REPORT_METRICS_MESSAGE { UINT32 SlotIndex; volatile LONG SequenceNumber; UINT16 Version; UINT16 Reserved0; UINT32 ReportRateHz; UINT32 AverageIntervalUs; UINT64 TimestampQpc; } IPC_INPUT_REPORT_METRICS_MESSAGE, *PIPC_INPUT_REPORT_METRICS_MESSAGE; #include <poppack.h>驱动用编译期断言强制校验布局假设(driver/DsInputReportMetrics.h):
C_ASSERT(sizeof(IPC_INPUT_REPORT_METRICS_MESSAGE) == 28); C_ASSERT((FIELD_OFFSET(IPC_INPUT_REPORT_METRICS_MESSAGE, SequenceNumber) % sizeof(LONG)) == 0); C_ASSERT((sizeof(IPC_INPUT_REPORT_METRICS_MESSAGE) % sizeof(LONG)) == 0);第二个断言保证SequenceNumber(seqlock 序号)按LONG对齐,使InterlockedIncrement等原子操作合法可用。发布者 DsInputReportMetrics_PublishValues 的写入顺序也遵循 seqlock 惯例:先InterlockedIncrement把序号变为奇数(写入中),再依次写入各字段,最后再次InterlockedIncrement使序号变为偶数(写入完成,快照一致)。槽位寻址采用offset = sizeof(结构) * (SlotIndex - 1),并按共享区域大小做越界保护(driver/DsInputReportMetrics.c)。
设备挂起/停止时(DsInputReportMetrics_Stop与DsInputReportMetrics_D0Entry)会发布全 0 快照或调用DsInputReportMetrics_PublishOrClearIpcSnapshot(..., TRUE)把整个槽位清零(SlotIndex = 0),用户态据此判断"无有效指标"。共享区域本身在 driver/IPC.c 中创建并记录缓冲基址与大小。
五、用户态读取:共享内存映射与 seqlock 拷贝
5.1 映射第四个内存区域
IPC 客户端DsHidMiniInterop在初始化时按分配粒度(dwAllocationGranularity)映射多个共享区域,指标区位于第 4 个区域,偏移为3 * allocationGranularity(SDK/Nefarius.DsHidMini.IPC/DsHidMiniInterop.cs)。该映射是可选的:旧版本驱动只暴露前三个区域,映射失败时HasInputReportMetrics为false,GetInputReportMetrics返回false。
5.2 读取 API:GetInputReportMetrics
核心读取入口在 SDK/Nefarius.DsHidMini.IPC/DsHidMiniInterop.Commands.cs:
public unsafe bool GetInputReportMetrics( int deviceIndex, out DsInputReportMetrics metrics, TimeSpan? timeout = null)deviceIndex:从 1 开始的设备索引;timeout:可选。不传时立即返回当前最新快照;传入时会在等待事件(GetOrOpenHidReportWaitEvent)上等待驱动发布下一版快照,若超时或槽位为空(SlotIndex == 0)则返回false;- 返回
false的三种典型场景:驱动无指标区域、槽位为空、等待超时。
内部通过TryCopyInputReportMetrics实现 seqlock 读取(DsHidMiniInterop.Commands.cs):先读序号(奇数则Thread.Yield()等待发布完成),拷贝结构体,再读一次序号,若两次不一致则重试;同时校验SlotIndex与请求的设备索引一致,否则抛出DsHidMiniInteropUnexpectedReplyException。这种"先验证写序、拷贝、再验证写序"的读法保证用户态始终拿到一致的快照,即使驱动侧正在并发写入。
5.3 版本属性与兼容性检查
驱动在设备属性中发布只读版本号DEVPKEY_DsHidMini_RO_InputReportMetricsVersion(DEVPROP_TYPE_UINT32,当前为1,见 driver/DsInputReportMetrics.c 的DsInputReportMetrics_AssignVersionProperty)。SDK 侧对应的静态属性为 DsHidMiniDriver.InputReportMetricsVersionProperty,建议在读取指标前先校验版本,再检查HasInputReportMetrics。
六、应用落地:ControlApp 中每秒轮询与格式化展示
指标最终落到用户可见的界面上,典型实现位于 ControlApp/ViewModels/UserControls/DeviceViewModel.cs:
- 设备视图模型启动一个每秒触发的定时器(
_inputReportMetricsQuery,初始延迟与周期均为 1 秒); - 每次触发先通过
TryReadInputReportMetricsVersion()读取设备属性的指标 ABI 版本(DeviceViewModel.cs),确认可用; - 通过惰性创建的
DsHidMiniInterop实例(EnsureInputReportMetricsInterop)调用HasInputReportMetrics与GetInputReportMetrics,取出metrics.ReportRateHz与metrics.AverageIntervalUs; - 交给格式化器渲染,仅在值发生变化时通过 Dispatcher 更新界面显示。
格式化规则见 ControlApp/Models/Input/InputReportMetricsFormatter.cs:
null(尚未读到)→"Unknown";0(窗口内无足够样本)→"—"(em dash);- 非零值:频率格式化为
"{0} Hz",间隔格式化为"{0:N0} µs"(千分位、微秒符号)。
这套 1 Hz 轮询 + 版本校验 + 可选读取的超时设计,使界面即使在指标暂不可用(如设备休眠、刚热插拔)时也能稳定降级显示,不会产生异常或误导性数据。
七、兼容性与使用注意事项
综合文档、SDK 与驱动源码,使用DsInputReportMetrics时有几点必须注意:
- 旧驱动不提供指标区:第四个共享区域与
InputReportMetricsVersionProperty均属于较新 ABI,对旧版驱动应先检查HasInputReportMetrics/ 版本属性再调用读取,SDK 的 README 亦明确"Absent on older drivers"。 - 数值语义:两个指标都覆盖"最近约 1 秒窗口",
0仅表示样本不足;不要将AverageIntervalUs当作链路单程延迟使用。 - 单次读取时机:快照约每秒发布一次,立即读取返回的是上一个已完成窗口;需要等待最新窗口时请传入
timeout参数。 - seqlock 一致性:用户态读取由
SequenceNumber奇偶校验保护,任何直接memcpy该共享区域而不校验序号的自行实现都是不安全的;应以 SDK 提供的GetInputReportMetrics为准。 - 按槽寻址:
SlotIndex从 1 开始且唯一对应一个设备插槽;读到SlotIndex == 0表示该槽无有效指标。
八、关键参考路径速查
- 文档主题模型:SDK/Nefarius.DsHidMini.IPC/Models/Public/InputReportMetrics.cs
- 驱动状态与发布:driver/DsInputReportMetrics.h、driver/DsInputReportMetrics.c
- 到达采集调用点:driver/DsHidMiniDrv.c(第 868、1038 行)
- 共享区域创建:driver/IPC.c
- 用户态读取 API:SDK/Nefarius.DsHidMini.IPC/DsHidMiniInterop.Commands.cs(
GetInputReportMetrics) - 区域映射与
HasInputReportMetrics:SDK/Nefarius.DsHidMini.IPC/DsHidMiniInterop.cs - 版本设备属性:SDK/Nefarius.DsHidMini.IPC/Models/Drivers/DsHidMiniDriver.cs(
InputReportMetricsVersionProperty) - 应用层展示:ControlApp/ViewModels/UserControls/DeviceViewModel.cs、ControlApp/Models/Input/InputReportMetricsFormatter.cs
- IPC 概览与兼容性说明:SDK/Nefarius.DsHidMini.IPC/README.md
围绕"报告到达率与平均间隔"这一指标,你已从数据结构、驱动统计窗口、共享内存 ABI、seqlock 无锁读取到界面轮询展示走通了完整链路。若需要在此基础上扩展(如把指标接入诊断导出或绘制时序曲线),可直接复用本文列出的各层入口继续深入。
- 驱动开发
- 硬件开发
【免费下载链接】DsHidMini
Virtual HID Mini-user-mode-driver for Sony DualShock 3 Controllers
相关推荐
深入bubblewrap命名空间:用户、PID、网络与IPC隔离详解
深入bubblewrap命名空间:用户、PID、网络与IPC隔离详解 在当今容器化技术蓬勃发展的时代, bubblewrap命名空间隔离 作为一种轻量级沙箱工具
容器运行时Windows-driver-samples 实战:用 KMDF 编写鼠标输入过滤驱动(Moufiltr)——报文报告链钩子与 PS/2 ISR 深入解析
Windows driver samples 实战:用 KMDF 编写鼠标输入过滤驱动(Moufiltr)——报文报告链钩子与 PS/2 ISR 深入解析 导读
示例工程如何让你的Mac鼠标体验飙升:从安装到付费的完整指南
如何让你的Mac鼠标体验飙升:从安装到付费的完整指南 Mac Mouse Fix是一款专为提升Mac用户鼠标体验设计的实用工具,它能帮助你自定义鼠标按钮功能、优
桌面应用系统编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考