diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md index 959d6db..cd4977b 100644 --- a/THIRD_PARTY_LICENSES.md +++ b/THIRD_PARTY_LICENSES.md @@ -4,7 +4,7 @@ This project uses the following third-party libraries and components: ## MaaFramework -- **Repository**: https://github.com/Ethernos-Studio/MaaFramework +- **Repository**: https://github.com/MaaXYZ/MaaFramework - **Path**: `third_party/maafw/` - **License**: GNU Lesser General Public License v3 (LGPL-3.0) - **License File**: `third_party/maafw/LICENSE.md` @@ -106,7 +106,7 @@ This project links against MaaFramework as a shared library (DLL on Windows). According to the LGPL-3.0 license: 1. The source code of MaaFramework is available at: - https://github.com/Ethernos-Studio/MaaFramework + https://github.com/MaaXYZ/MaaFramework 2. Users can replace the MaaFramework DLL with a modified version that is compatible with the LGPL-3.0 license. @@ -158,7 +158,7 @@ In compliance with GNU licenses (AGPL-3.0, LGPL-3.0, LGPL-2.1+), source code for the project and its LGPL dependencies can be obtained from: - **AAM**: https://github.com/Ethernos-Studio/ArknightsAutoMachine -- **MaaFramework**: https://github.com/Ethernos-Studio/MaaFramework +- **MaaFramework**: https://github.com/MaaXYZ/MaaFramework - **FFmpeg**: https://github.com/FFmpeg/FFmpeg See `docs/licenses/SOURCE_CODE_ACCESS.md` in the distribution for details. diff --git a/USER_AGREEMENT.md b/USER_AGREEMENT.md index 03457c4..53554bb 100644 --- a/USER_AGREEMENT.md +++ b/USER_AGREEMENT.md @@ -1,4 +1,4 @@ - + @brief Arknights Auto Machine (AAM) 综合用户协议与技术边界声明 + ============================================================================= --> -# Arknights Auto Machine (AAM) 用户协议 +# Arknights Auto Machine (AAM) 综合用户协议与技术边界声明 -**版本**:v1.0.0 -**生效日期**:2026-04-05 -**版权所有**:Ethernos Studio (dhjs0000) +**版本**:v2.2.0 +**生效日期**:2026-04-07 +**版权所有**:Ethernos Studio **开源许可**:GNU Affero General Public License v3.0 (AGPL-3.0-only) --- -## 1. 接受条款与风险提示 +## 前言:技术中立性与法律合规性的分离声明 -1.1 使用 AAM(以下简称"本软件")即表示您已阅读、理解并同意本协议所有条款。若不同意,请立即卸载并停止使用。 +**重要认知前提**:本软件基于通用计算机视觉技术与自动化控制框架构建,其技术组件(包括但不限于 **YOLO 实时目标检测**、ONNX 运行时、进程间通信、大语言模型接口)在无障碍辅助、自动化测试、工业视觉检测等领域具有合法且广泛的应用价值。然而,技术的中立性并不等同于特定场景下的合规性。 -1.2 **重要风险提示**: -- 本软件通过 ADB 协议模拟人类操作行为,**必然违反**《明日方舟》用户协议第 4.3 条关于禁止自动化程序的规定 -- 使用本软件可能导致游戏账号受到**警告/临时封禁/永久封禁**等处罚 -- 鹰角网络保留随时更新反作弊机制的权利,本软件的规避检测能力**不具有持久性保证** +您即将使用的 AAM 软件,尽管在技术实现上采用了**非侵入式架构**(仅通过屏幕捕获与模拟输入交互,不修改游戏内存或网络协议),且使用 **YOLOv8/v9 模型**仅进行**视觉层面的目标检测**(不读取游戏内部对象数据),但其应用于《明日方舟》游戏场景时,**必然构成对《鹰角网络游戏使用许可及服务协议》第 6.2 条第 16 款的实质性违反**。该条款明确禁止"通过非鹰角网络开发的或未经授权的第三方软件、插件、外挂、系统,使用鹰角网络游戏软件及鹰角网络游戏服务"。 -1.3 **AI 决策特别声明**: -AAM L5 层可能接入第三方大语言模型(LLM)API(包括但不限于 Kimi、Claude 等)。AI 生成的操作指令可能存在**误判、延迟或异常行为**,开发者不对 AI 决策导致的游戏损失承担责任。 +本协议旨在详尽阐述技术架构的边界(包括 **YOLO 模型的输入输出限制**)、第三方依赖的合规义务、用户行为的法律风险,以及开发者的责任限制。通过签署本协议,您确认已充分理解技术实现细节与法律定性之间的分离关系,并自愿承担由此产生的全部风险。 --- -## 2. 知识产权与开源声明 +## 第一章 系统架构与技术边界详述 + +### 1.1 分层架构的技术解剖与 YOLO 集成 + +AAM 采用模块化分层设计,各层的功能边界与技术约束如下,**特别标注 YOLO 在视觉感知层的作用与限制**: + +**L0 Sensing 层(感知层)** +本层负责与外部世界的原始交互。在 Android 平台上,它通过 **Android Debug Bridge (ADB) 协议**执行屏幕截图捕获与触控事件注入。技术边界声明:**本层不调用任何 Windows API 读取游戏进程内存(如 `ReadProcessMemory`),不注入 DLL 到游戏进程,不挂钩(Hook)系统或游戏的图形渲染 API(如 DirectX、OpenGL ES 拦截)**。共享内存(Shared Memory, SHM)仅用于 AAM Core (C++) 与 AAM Inference (Python) 进程间的内部数据交换,**绝不与《明日方舟》游戏进程共享内存空间**。 + +**L1 Perception 层(感知处理层)** +本层是 AAM 的视觉处理核心,采用**双轨架构**: + +- **传统 CV 轨道**:基于 **OpenCV 4.x**(Apache-2.0 许可证)进行模板匹配与 OCR(光学字符识别),用于识别固定 UI 元素(如费用数值、干员头像、按钮区域); +- **YOLO 深度学习轨道**:基于 **Ultralytics YOLOv8/v9**(AGPL-3.0 许可证,与 AAM 许可证兼容)的 **ONNX 导出模型**,执行**实时目标检测**(Real-time Object Detection)。 + +**YOLO 技术细节与边界**: +- **输入**:屏幕截图(由 L0 层通过 ADB 捕获,16-33ms 延迟); +- **检测类别**:敌方单位(按职业分类:领袖、重装、术师等)、我方干员部署位置( bounding box 坐标)、技能就绪状态图标、特殊机制物体(如装置、障碍物); +- **模型局限**:**纯视觉识别,不读取游戏内部实体 ID 或属性数值**;依赖 GPU 加速(CUDA/DirectML),在低端设备上可能存在 50-200ms 的推理延迟;存在**误检/漏检风险**(如相似敌人外观混淆、密集重叠单位漏检); +- **卫戍协议特定**:YOLO 用于识别**随机出现的敌人类型**(如领袖单位、特训敌人)的**视觉特征**(模型通过训练数据识别特定敌人的像素模式),但**无法预测**敌人的波次生成逻辑或领袖技能释放时机。 + +**L2 Motor 层(执行层)** +通过 **MaaFramework**(LGPL-3.0 许可证)提供的接口,将 L3/L5 层的决策转换为 ADB 命令序列,模拟人类手指的点击、滑动、长按等操作。操作执行引入随机延迟与坐标扰动("人性化模拟"),但**不构成对游戏客户端的代码注入或协议层伪造**。 + +**L3 Tactical 层(战术决策层)** +内置 **DSL(领域特定语言)编译器**,解析基于 **公开数据源**(PRTS Wiki、GamePress、官方攻略等社区维护的静态数据库)的关卡机制、敌人路线与属性数值,进行**预计算战术规划**。 + +**卫戍协议特定修正**:对于「卫戍协议」模式,L3 层**无法使用静态数据库**(因地图随机选择、领袖随机、盟约组合多变),此时系统**降级为 YOLO 视觉反应模式**:L1-YOLO 层实时检测战场敌情(敌人位置、生命值状态),L3 层基于**视觉反馈**而非**预计算**生成战术指令(如"检测到领袖进入射程则部署干员")。这是一种**反应式(Reactive)**而非**预测式(Predictive)**的战术逻辑,**效率显著低于固定关卡的预计算模式**。 + +**L4 State 层(状态管理层)** +维护游戏状态机的快照序列化,使用 **Protocol Buffers**(BSD-3-Clause 许可证)进行数据持久化与进程间通信(通过 **gRPC**,Apache-2.0 许可证)。本层数据**仅反映通过屏幕识别(含 YOLO 检测结果)推断的状态,而非游戏内部的真实内存状态**。 + +**L5 Strategy 层(战略决策层)** +通过 **Bridge 层(gRPC / ZeroMQ, MPL-2.0 许可证)**接收 L4 层上传的结构化状态描述(含 YOLO 检测到的敌方单位列表),调用本地或远程的大语言模型(LLM)/视觉语言模型(VLM)API 生成高层战略建议。**在卫戍协议的多人模式下,L5 层仅接收本机阵地状态,无法获取其他玩家(同盟)的完整战场信息,可能导致协同策略失误**。 + +**Data 层(数据层)** +存储干员属性、敌人图鉴、固定关卡路线等**公开可用数据**,使用 **nlohmann/json**(MIT 许可证)进行解析。**明确排除**对游戏客户端私有资源包(.ab 文件)的逆向解包或加密协议分析。 + +### 1.2 第三方依赖的技术栈与合规基础 + +AAM 的构建与运行依赖以下关键第三方开源组件,其许可证合规性直接影响本软件的分发与使用: + +**核心自动化框架:MaaFramework** +- **许可证**:GNU Lesser General Public License v3 (LGPL-3.0) +- **交互方式**:动态链接(Shared Library/DLL) +- **合规要点**:根据 LGPL-3.0 第 4 条,AAM 作为独立作品通过动态链接使用 MaaFramework,**不构成衍生作品**。用户有权获取 MaaFramework 的源代码( https://github.com/MaaXYZ/MaaFramework ),并有权替换 AAM 发行版中附带的 `MaaFramework.dll` 或 `libMaaFramework.so` 为自行修改的兼容版本。AAM 的 AGPL-3.0 许可证**不扩展至** MaaFramework 本身,但修改 MaaFramework 并分发的行为需遵守 LGPL-3.0。 -2.1 本软件著作权归 Ethernos Studio 所有,受《中华人民共和国著作权法》及国际版权公约保护。 +**实时目标检测:Ultralytics YOLOv8/v9** +- **许可证**:GNU Affero General Public License v3.0 (AGPL-3.0) +- **集成方式**:AAM 使用 YOLO 的 **ONNX 导出模型**(`yolov8n.onnx` 或定制训练模型)通过 **ONNX Runtime**(MIT 许可证)进行推理,**不直接链接 YOLOv8 的 Python 库或 C++ 源码**。 +- **合规要点**:尽管 AAM 使用 ONNX 模型而非直接集成 YOLO 代码,但 YOLO 模型的训练与导出流程受 AGPL-3.0 约束。AAM 提供的预训练模型(如针对《明日方舟》优化的 `.onnx` 文件)其权重参数**不构成软件源代码**,但如果您修改并重新分发 YOLO 训练代码本身,需遵守 AGPL-3.0。 +- **模型限制声明**:YOLO 模型仅用于**学术研究与无障碍辅助技术验证**,其检测精度受限于训练数据集(仅包含公开游戏截图),**不保证对所有敌人类型(特别是新活动首次出现的敌人)的识别准确率**。 -2.2 本软件以 **AGPL-3.0-only** 开源许可证发布。根据 AGPL: -- 您有权自由使用、修改、分发本软件 -- **网络交互限制**:若您基于本软件提供在线服务(如云托管、SaaS 平台),必须向所有用户公开服务端源代码 -- **传染性**:任何集成/修改后的衍生作品必须沿用 AGPL 许可证开源 +**媒体处理:FFmpeg** +- **许可证**:LGPL-2.1+ 或 GPL-2.0+(取决于编译配置) +- **合规要点**:AAM 通过 vcpkg 引入 FFmpeg,仅使用其解码/编码功能(非 GPL 组件如 x264 可选)。用户有权获取 FFmpeg 源代码( https://github.com/FFmpeg/FFmpeg )并替换二进制文件。 -2.3 **Maa 兼容性声明**: -AAM 实现了与 MaaAssistantArknights 的接口兼容,但**不包含** Maa 的受版权保护资源文件(如内置识别模型、GUI 素材)。Maa 相关资源受其原 AGPL 许可证约束。 +**基础设施库**: +- **OpenCV**:Apache-2.0 许可证(专利授权条款) +- **ONNX Runtime**:MIT 许可证(用于 YOLO 模型推理) +- **gRPC**:Apache-2.0 许可证(RPC 框架) +- **ZeroMQ/libzmq**:Mozilla Public License 2.0 (MPL-2.0)(文件级传染性,与 AAM 的 AGPL 兼容) +- **Protocol Buffers**:BSD-3-Clause 许可证 +- **fmt, spdlog, cxxopts, cppzmq, nlohmann/json**:MIT 许可证(宽松,需保留版权声明) +- **QuickJS**(MaaFramework 依赖):MIT 许可证 +- **tl-expected**:CC0-1.0(公共领域) + +上述依赖的许可证文本与源代码获取方式详见随软件分发的 `docs/licenses/` 目录及 `THIRD_PARTY_LICENSES.md` 文件。 --- -## 3. 禁止行为(绝对禁止清单) +## 第二章 游戏特定性风险与技术迁移警告 + +### 2.1 跨游戏适用的风险梯度 + +**核心风险告知**:AAM 的 L0-L5 架构(含 **YOLO 视觉检测**)在技术上具有**跨游戏通用性**。若用户将此技术栈应用于含有**玩家间实时竞技(PvP)**机制的游戏(如第一人称射击游戏《Valorant》《CS2》、多人在线战术竞技游戏《英雄联盟》《DOTA2》),L1-YOLO 层的实时目标检测(自动瞄准、敌人位置追踪)与 L3 层的战术决策将立即构成**破坏竞技公平性的"人工智能外挂"**(如实时锁定敌方玩家头部坐标)。 + +然而,AAM 的当前实现针对《明日方舟》的特定技术环境进行了**约束性适配**,其风险等级差异如下: + +| 技术维度 | 传统竞技游戏"外挂"(高风险) | AAM 在《明日方舟》中的实现(中低风险) | +|---------|------------------|-------------------------| +| **数据输入源** | 内存读取(DMA、Kernel Driver)、网络协议拦截(Man-in-the-Middle)、渲染层注入(Overlay Hook) | **仅通过 ADB 获取屏幕像素流**(YOLO 处理视觉帧),**零内存读取**,**零网络层注入** | +| **预测/检测基础** | 实时计算敌方玩家位置、技能冷却、经济状态(非公开数据,需破解加密) | **固定关卡**:基于 **已公开的敌人数据库** 预计算;**卫戍协议**:YOLO **实时检测**已出现的敌人视觉特征(仅反应,不预测生成逻辑) | +| **交互方式** | 内核级输入(驱动级鼠标移动、硬件级宏)、数据包伪造(瞬移、加速) | **仅模拟人类触控操作**(点击、滑动),通过 ADB 协议传输,**不涉及内核驱动或协议伪造** | +| **联机影响** | 直接破坏 PvP 竞技公平性,影响其他玩家体验 | 游戏核心为**单人/协作 PvE**;「卫戍协议」为**协作对抗 AI**,YOLO 仅辅助识别,**不控制其他玩家阵地**,且 L3 层**无法处理**多人协同的复杂策略(如联防阶段决策) | +| **经济系统** | 操控交易市场、自动套利、影响其他玩家经济利益 | **仅影响用户自身账号的本地资源消耗**,**不涉及玩家间交易操控** | + +### 2.2 《明日方舟》游戏性质的特定性依赖 + +您明确理解并同意,AAM 当前被界定为"**高级自动化脚本**"而非"**竞技作弊工具**",**严格依赖于**《明日方舟》的以下技术特性: + +1. **半单机架构**:核心玩法(主线关卡、资源副本、剿灭作战、集成战略)基于**本地静态数据**运行,服务器仅作结果校验与数据持久化。L3 层的 DSL 编译器可解析**固定关卡配置**(如敌人行进路线、波次 timing、固定机制触发点)。 -使用本软件时,您**不得**: +2. **卫戍协议的视觉辅助限制**:「卫戍协议」模式虽涉及多人协作与随机元素(随机领袖、随机地图、70秒决策时间限制),但其本质仍为**玩家协作对抗 AI(PvE)**。AAM 对此模式的支持**仅限于**: + - **L1-YOLO 实时检测**:识别当前战场上的敌人类型(领袖、特训敌人)与位置; + - **L3 反应式战术**:基于 YOLO 检测结果执行简单逻辑(如"费用充足且检测到领袖则部署银灰"); + - **明确排除**:**无法预测**随机地图布局(因地图在开局后随机选择,非固定数据库可预载);**无法控制**其他玩家的阵地(同盟模式下仅能操作本机干员);**无法处理**"联防阶段"的跨阵地协同(因涉及其他玩家状态,超出 YOLO 视觉范围)。 -3.1 **商业利用**:出售本软件、提供代练服务、或以任何方式通过本软件直接/间接牟利(包括但不限于售卖"防封版"、提供付费挂机服务) +3. **公开数据依赖**:Data 层使用的干员属性数据库、敌人图鉴、固定关卡路线均来源于**社区公开维护的数据集**(PRTS Wiki、Arknights Terra Wiki、GamePress 等),**非通过逆向工程(Reverse Engineering)或反编译获取的加密资产**。 -3.2 **逆向与破解**:反编译、反汇编、修改本软件二进制以绕过 AGPL 开源要求;开发"内存注入版""协议破解版"等增强模块 +4. **非竞技性(Non-PvP)**:截至本协议生效日,《明日方舟》**不存在**玩家间直接对抗的排位赛、竞技场或经济对抗系统。「卫戍协议」的同盟模式虽有多人元素,但为**协作性质**,且 AAM **不保证**在多人环境下的决策有效性(如因网络延迟导致的 70 秒决策超时、因无法观察友方阵地导致的部署冲突)。 -3.3 **数据滥用**:利用 AAM 的图像识别功能批量采集游戏数据并用于训练商业 AI 模型;将游戏截图(可能含个人信息)上传至未授权的第三方 AI 服务 +### 2.3 游戏机制变更的兼容性警告与风险升级 -3.4 **规避检测的恶意修改**:使用本软件配合硬件宏、虚拟机沙箱、或修改游戏客户端以逃避反作弊检测 +**关键风险提示**:若《明日方舟》在未来版本更新中引入以下机制,AAM 的技术性质可能发生**法律风险升级**,您有义务立即停止使用: -3.5 **法律责任转嫁**:将本软件用于《刑法》第 285 条禁止的"破坏计算机信息系统"行为(如利用自动化工具实施游戏内诈骗、刷取虚拟货币并变现) +- **玩家间实时竞技(PvP)模式**:如排位竞技场、玩家对战玩法; +- **服务器端动态生成关卡**:如完全随机的地图布局、实时变化的敌人属性,使得 L3 层的静态数据库与 YOLO 的训练模型**同时失效**; +- **玩家间自由交易市场**:如允许玩家间交易虚拟货币或物品; +- **反作弊机制升级**:鹰角网络明确将"基于计算机视觉的自动化检测"(YOLO 实时识别)或"ADB 模拟触控"纳入行为检测范围,或开始封禁 ADB 调试接口的使用。 + +在上述情况下,继续使用 AAM 将**从"违反服务协议的自动化脚本"升级为"破坏游戏公平性的作弊工具"**,可能导致**更严厉的账号处罚**(永久封禁且不予申诉),甚至可能涉及**不正当竞争**的法律风险。 --- -## 4. 隐私与数据处理 +## 第三章 许可证合规与开源义务 + +### 3.1 AGPL-3.0 的刚性传染性与网络服务条款 + +AAM 以 **GNU Affero General Public License v3.0 (AGPL-3.0-only)** 授权。该许可证在标准 GPL 基础上增加了**网络交互传染性条款**(第 13 条),对您的使用与分发行为施加以下义务: + +**源代码可得性**:无论以何种方式获得本软件(源码或二进制),您必须保留完整的源代码获取方式( https://github.com/Ethernos-Studio/ArknightsAutoMachine ),并允许接收者以不高于合理成本的价格获取。 + +**网络服务传染性**:若您基于 AAM 提供**网络服务**(包括但不限于云托管的自动化服务、SaaS 平台、在线代练接口、远程挂机网页等),您必须向该服务的**所有用户**(包括通过网页界面交互的终端用户)公开**完整的服务端源代码**,无论其是否直接与您建立合同关系。此义务**不因您修改或不修改 AAM 源代码而免除**。 + +**许可证不可变更**:任何包含或衍生自 AAM 的作品(包括但不限于插件、扩展、封装器、图形界面前端、自定义 YOLO 训练脚本)必须继续沿用 AGPL-3.0(或更高版本)许可证,**不得添加额外限制条款**(如禁止商业使用、禁止特定领域使用等),**亦不得降级为更宽松的许可证**(如 MIT、Apache-2.0、BSD 等)。 + +### 3.2 第三方依赖的合规链条 + +**MaaFramework (LGPL-3.0) 的动态链接合规**: +AAM 通过动态链接(Windows 下为 `MaaFramework.dll`,Linux 下为 `libMaaFramework.so`)使用 MaaFramework。根据 LGPL-3.0 第 4(d) 条,您有权: +1. 获取 MaaFramework 的完整源代码( https://github.com/MaaXYZ/MaaFramework ); +2. 使用修改后的兼容版本替换 AAM 发行版附带的 MaaFramework 二进制文件; +3. 要求任何分发修改版 MaaFramework 的主体遵守 LGPL-3.0(但 AAM 本身不因此成为 LGPL 作品)。 + +**Ultralytics YOLO (AGPL-3.0) 的模型合规**: +AAM 分发预训练的 YOLO 模型文件(`.onnx` 格式)用于敌方单位检测。根据 AGPL-3.0: +1. 若您修改 YOLO 的训练代码(如添加新的数据增强算法)并分发修改后的代码,需开源; +2. 仅使用预训练模型进行推理(如 AAM 的做法)**不触发** AGPL 的源代码公开义务,但模型的训练脚本(如数据标注脚本、训练配置)若集成到 AAM 仓库中,需随 AAM 一同 AGPL 开源。 + +**MIT/Apache/BSD 许可证的版权保留**: +对于 OpenCV、ONNX Runtime、fmt、spdlog、gRPC、nlohmann/json 等宽松许可证依赖,您必须保留其原始版权声明与许可文本,**不得移除或篡改** `third_party/` 目录下的许可证文件。 + +**ZeroMQ (MPL-2.0) 的文件级隔离**: +ZeroMQ 采用文件级弱传染性许可证。AAM 通过独立文件(`cppzmq` 绑定)使用 ZeroMQ,**不构成与 AAM 的合并作品**,但修改 ZeroMQ 源代码文件本身需遵守 MPL-2.0。 + +### 3.3 商业利用的禁止与 AGPL 的兼容性 + +尽管 AGPL-3.0 允许商业使用,但**本协议第 6 条(禁止行为)明确禁止**您通过 AAM 提供付费代练、售卖"防封版"或构建闭源 SaaS 平台。 +**重要提示**:若您违反第 6 条进行商业牟利,您同时违反了 AGPL-3.0 的"相同方式共享"条款(因为您未向服务用户公开源代码),此时 **Ethernos Studio 有权追究您侵犯著作权的责任**,同时您也失去了 AGPL 授予您的技术使用许可。 + +--- + +## 第四章 隐私保护、数据流与 AI 决策风险 + +### 4.1 本地优先的数据处理架构与 YOLO 隐私边界 + +AAM 默认采用**端侧计算优先**架构,数据流如下: + +**L0-L4 层(C++ Core)**: +- 屏幕截图(通过 ADB)仅在本地内存中处理,**不上传**至 Ethernos Studio 服务器; +- **YOLO 推理**:在本地 GPU(NVIDIA CUDA 或 DirectML)上执行,**原始图像帧不离开本机**; +- OCR 识别结果、YOLO 检测框坐标(bounding boxes)、游戏状态推断数据通过 **ZeroMQ (MPL-2.0)** 或 **gRPC (Apache-2.0)** 在本地进程间(Core ↔ Inference)传输,**不经过互联网**; +- 操作日志(含 YOLO 检测结果摘要,如"检测到领袖:爱国者")仅本地保存,默认 **7 日后自动不可逆清除**。 + +**L5 层(Python Inference)**: +- 仅在您**主动启用** AI 决策功能时,结构化状态描述(含 YOLO 检测到的敌方列表、干员部署状态、费用数值,**不含原始屏幕截图**)通过 **HTTPS/TLS 1.3** 传输至您配置的第三方 LLM API(如 Moonshot AI、Anthropic Claude、OpenAI GPT 等); +- **强烈建议**在配置文件中启用 `privacy_mode: strict`,该模式将自动模糊处理用户 ID、充值记录、社交关系图谱等敏感元数据,**仅传输最小必要信息**(如仅发送"领袖已出现"而非具体坐标)。 + +### 4.2 第三方 AI 服务的隐私边界 + +若您启用 L5 层: +- 您需**自行审阅**所配置 API 提供商的隐私政策(如 Moonshot AI 的《用户协议》与《隐私政策》),Ethernos Studio **不对**第三方服务的数据泄露、模型训练数据滥用或跨境数据传输合规性承担责任; +- 建议优先使用**本地部署**的开源大模型(如 Llama 3、Qwen 2.5)以避免云端数据暴露; +- 明确禁止将包含其他玩家个人可识别信息(PII)的游戏截图(如多人模式中的玩家昵称、UID)传输至第三方 AI 服务。 + +### 4.3 AI 决策与 YOLO 检测的局限性与风险披露 + +**L1-YOLO 检测的固有局限**: +- **误检/漏检**:YOLO 模型基于固定训练数据集(历史活动截图),对于**首次出现的新敌人**(如新一期「卫戍协议」的新领袖)可能无法识别或错误分类(如将"萨卡兹百夫长"误检为"萨卡兹刀兵"); +- **遮挡与重叠**:在敌人密集或特效遮挡(如全屏技能动画)时,YOLO 可能漏检关键单位,导致 L3 层战术失效; +- **分辨率依赖**:YOLO 模型对特定屏幕分辨率(如 1920x1080、2560x1440)训练,若您使用非标分辨率(如 21:9 超宽屏),检测精度可能下降; +- **卫戍协议特定**:70 秒决策时间限制下的**实时推理压力**(若 GPU 性能不足,YOLO 推理可能占用 1-2 秒,压缩后续决策时间);**多人模式下仅检测本机阵地**,无法观察友方部署导致的**协同失误**(如重复部署同类型干员)。 + +**L3 层战术局限(卫戍协议特定)**: +- **反应式延迟**:对于「卫戍协议」,L3 层基于 YOLO 检测结果采取**事后反应**(敌人出现后才响应),而非固定关卡的**事前预部署**,**胜率显著低于人工操作**; +- **无法处理随机盟约**:L3 层无法理解复杂的"盟约"组合效果(如特定阵营增益),可能做出次优决策; +- **无法处理联防阶段**:当进入"联防阶段"(合并失败玩家剩余敌人)时,AAM **无法识别**友方阵地的干员配置(因仅捕获本机屏幕),可能导致部署冲突或资源浪费。 + +**L5 层 LLM/VLM 决策的固有局限**: +- **幻觉(Hallucination)风险**:大语言模型可能错误解读 YOLO 检测结果(如将"领袖已进场"误判为"领袖已死亡"),生成完全错误的部署指令; +- **上下文限制**:LLM 可能无法处理「卫戍协议」的长局记忆(如 20+ 回合的累积经济状态),导致后期决策失误。 + +**开发者明确声明**:**不对因 YOLO 误检、L3 反应式战术局限、或 L5 层 AI 决策失误导致的游戏内损失承担责任**,包括但不限于错误部署高价值干员、误消耗珍稀道具、关卡失败导致的理智值损失或活动奖励遗漏。 + +--- + +## 第五章 责任限制与广泛范围免责声明 + +### 5.1 无担保的"现状"提供(AS IS) + +**在适用法律允许的最大范围内**,本软件按"**现状**"(AS IS)及"**可用即提供**"(AS AVAILABLE)的基础提供,不附有任何形式的明示、默示或法定担保。Ethernos Studio 明确否定以下担保: + +- **适销性默示担保**:不保证本软件适合任何特定商业用途或通用目的,不保证其与特定操作系统版本(如未来版本的 Android 或 iOS)或硬件配置(特定分辨率、DPI 设置、GPU 型号)的兼容性; +- **特定用途适合性默示担保**:不保证本软件满足您的特定游戏策略、胜率预期或效率指标,**不保证其能够完成任何特定关卡(特别是「卫戍协议」等随机性强的模式)的通关**; +- **非侵权担保**:不保证使用本软件不会侵犯鹰角网络、Hypergryph、Ultralytics 或任何第三方的知识产权、合同权利或其他合法权益; +- **技术缺陷担保**:明确告知本软件可能存在未发现的软件缺陷(Bug)、YOLO 模型误检、逻辑漏洞、内存泄漏或与特定反病毒软件的误报冲突。 + +### 5.2 损失免责的广泛范围 + +**在适用法律允许的最大范围内,且无论 Ethernos Studio 是否已被告知可能发生该等损失**,Ethernos Studio 及其创始人 dhjs0000 在任何情况下均不对以下损失承担责任: + +**直接损失**: +- **账号与虚拟财产损失**:游戏账号被鹰角网络封禁(临时或永久)、信誉分降低、虚拟道具被回收、合成玉/源石/龙门币等虚拟货币被清零、游戏进度被回滚或删除; +- **硬件与数据损失**:因运行 AAM(特别是 YOLO 的 GPU 高强度计算)导致的移动设备过热、电池损耗加速、显卡(若通过模拟器运行)损坏、操作系统崩溃、个人数据丢失或需要重装系统; +- **AI 与视觉检测级联失误**:因依赖 L1-YOLO 误检(如漏检领袖单位)、L3 反应式战术局限(卫戍协议中的随机性应对失败)、L5 层 AI 误判导致的干员部署错误、费用管理失误、技能开启时机错误、关卡失败、活动奖励遗漏或游戏内经济系统损失。 + +**间接与衍生损失**: +- **机会成本损失**:因账号封禁导致的游戏内活动参与权丧失、排名奖励无法获取; +- **数据恢复成本**:为恢复被封禁账号或丢失数据产生的律师费、申诉代理费、时间成本; +- **替代软件采购成本**:因 AAM 失效(如 YOLO 模型过旧无法识别新敌人)而需购买的其他自动化工具费用; +- **情感与机会成本**:因使用本软件产生的焦虑、失望、时间浪费或游戏体验降低。 -4.1 **本地优先原则**: -AAM 默认在本地设备运行所有图像识别与逻辑推理(L0-L4 层)。游戏画面截图**不会**上传至 Ethernos Studio 服务器。 +**第三方行为后果**: +- 因鹰角网络政策变更、反作弊系统升级(如开始检测 ADB 或视觉自动化行为)、游戏版本更新(如 Unity 引擎升级导致图像渲染变化使 YOLO 模型失效)导致的软件功能失效; +- 因第三方 AI 服务(Kimi、Claude 等)中断、API 密钥失效、模型版本更新(如模型能力下降)导致的 L5 层决策质量降低。 -4.2 **LLM 服务数据流**: -若您启用 L5 层 AI 决策功能: -- 游戏画面截图/状态描述将发送至您配置的第三方 AI API(如 Kimi Code) -- 您需自行审阅该 API 提供商的隐私政策,Ethernos Studio 不对第三方数据泄露负责 -- 建议在配置文件中启用 `privacy_mode: strict`,限制截图中的敏感信息(如 UID、充值记录)传输 +### 5.3 责任上限的刚性约束 -4.3 **数据留存**: -AAM 仅在本地保存操作日志(用于调试),不会上传用户游戏数据。日志文件默认保存 7 天,到期自动清理。 +若适用法律强制要求 Ethernos Studio 承担赔偿责任(且本协议第 5.2 条的免责条款被有权机关认定为部分或全部无效),赔偿金额**在任何情况下均不超过**以下两者中的较高者:(1)您为获得本软件实际支付的金额(鉴于 AGPL 开源性质,该金额通常为 **0 元人民币**);(2)**人民币 50 元整**。 + +### 5.4 用户自担风险的确认 + +您明确确认并同意:**使用本软件参与《明日方舟》游戏的所有后果均由您独立承担**。您与鹰角网络之间的"猫鼠博弈"(即自动化操作与反作弊检测之间的技术对抗)风险**完全由您承担**。您不会因任何负面结果(包括但不限于账号封禁、虚拟财产损失、YOLO 误检导致的战术失败)向 Ethernos Studio 提出任何形式的索赔、诉讼、仲裁或投诉。 --- -## 5. 责任限制与免责声明 +## 第六章 禁止行为清单(零宽容政策) + +任何违反本条的行为将导致本协议**立即自动终止**,并可能触发 AGPL-3.0 许可证下的著作权侵权追责: + +**6.1 商业牟利行为** +严禁出售 AAM 副本、提供付费代练/挂机服务、构建基于 AAM 的付费 SaaS 平台而不履行 AGPL 开源义务;严禁售卖所谓"防封版""内存注入版""协议破解版"或"YOLO 增强付费版"。 -5.1 **无担保声明**: -本软件按"**现状**"(AS IS)提供,不附有任何明示或默示担保,包括但不限于: -- 对游戏账号安全的担保 -- 对反作弊规避持久性的担保 -- 对 AI 决策准确性的担保 +**6.2 规避 AGPL 传染性的技术规避** +通过动态链接之外的机制(如静态链接、内联汇编、代码混淆)将 AAM 或 YOLO 集成到闭源商业软件中,以规避 AGPL-3.0 的网络服务传染性要求。 -5.2 **损失免责**: -在任何情况下,Ethernos Studio 及开发者 dhjs0000 均不对以下损失承担责任: -- 游戏账号被封禁导致的虚拟财产损失 -- 因使用本软件导致的设备损坏、数据丢失 -- 依赖 AI 决策导致的游戏内资源损失(如错误部署干员导致关卡失败) +**6.3 游戏机制恶意利用** +利用 AAM 的自动化能力实施《中华人民共和国刑法》第 285 条、第 286 条禁止的"破坏计算机信息系统"行为,包括但不限于: +- 利用自动化工具实施游戏内诈骗; +- 非法刷取虚拟货币并变现(RMT,Real Money Trading); +- 干扰游戏服务器正常运行(如通过高频 ADB 命令或 YOLO 高频截图造成服务器负载异常)。 -5.3 **责任上限**: -若适用法律要求承担赔偿责任,赔偿金额**不超过**您为本软件实际支付的费用(鉴于 AGPL 开源性质,通常为 0 元)。 +**6.4 数据滥用与 AI 训练** +利用 AAM 的图像采集功能(包括 YOLO 检测数据集)批量提取游戏数据并用于训练**商业闭源 AI 模型**;将包含用户 PII 的游戏截图传输至未获明确授权的第三方 AI 服务。 + +**6.5 恶性反检测协同** +将 AAM 与硬件宏脚本、内核级驱动(Kernel Driver)、虚拟机沙箱逃逸技术或修改后的游戏客户端协同使用,以逃避反作弊检测。 + +**6.6 YOLO 模型的恶意修改与分发** +训练包含恶意后门的 YOLO 模型(如故意误检特定敌人以诱导失败)并通过 AAM 社区分发;使用未经授权的第三方商业 YOLO 模型(可能涉及版权侵权)集成到 AAM 中。 --- -## 6. 协议更新与终止 +## 第七章 协议更新、终止与争议解决 -6.1 **更新权**:Ethernos Studio 保留随时修改本协议的权利。重大变更将在 GitHub Releases 页面公示 7 日,继续使用视为接受新条款。 +### 7.1 协议的动态更新权 +Ethernos Studio 保留在不事先通知的情况下单方面修改本协议的权利。若《明日方舟》服务条款更新(特别是反作弊政策收紧,如明确禁止视觉自动化)、游戏机制变更(如引入 PvP),或 YOLO 模型许可证(Ultralytics 政策变更),本协议将相应更新以反映**风险等级变化**,并在 GitHub Releases 页面及项目仓库公示不少于 **7 个自然日**。 -6.2 **自动终止**: -若您违反第 3 条禁止行为,本协议自动终止,AGPL 赋予的开源权利**不受影响**(您仍可保留、修改代码,但失去获得技术支持的资格)。 +### 7.2 自动终止条件 +若您: +(1)违反第 6 条任何禁止行为; +(2)将 AAM 技术栈(含 YOLO 视觉检测)迁移至含 PvP 元素的其他游戏; +(3)在《明日方舟》引入 PvP 模式后继续使用 AAM; +本协议将在该等行为发生之时**自动终止**,无需另行通知。**协议终止不影响 AGPL-3.0 授予您的开源权利**(保留、修改、分发源代码的权利),但您永久失去获得技术支持与社区协助的资格。 -6.3 **用户终止权**:您可随时停止使用并卸载本软件, uninstall 行为即视为协议终止。 +### 7.3 法律适用 +本协议适用**中华人民共和国大陆地区法律**(不含冲突法规则)。 --- -## 7. 争议解决与适用法律 +## 第八章 最终确认条款(GUI 界面专用) -7.1 本协议适用中华人民共和国法律(不含冲突法规则)。 +**强制性确认按钮文案**(用户必须滚动阅读全文后方可点击): + +> **"我已完整阅读、充分理解并接受《Arknights Auto Machine 综合用户协议》(v2.1.0-YOLO-Enhanced)的全部条款,包括:** +> +> **(1)技术架构理解**:我确认理解 AAM 的 L0-L5 分层架构,特别是 **L1-YOLO 层** 使用 Ultralytics YOLO 模型进行实时目标检测(识别敌人/干员位置)的技术边界与误检风险;我理解对于「卫戍协议」模式,YOLO 仅提供**反应式视觉辅助**(检测已出现的敌人),**无法预测**随机地图布局或处理多人协同策略; +> +> **(2)许可证合规义务**:我承诺遵守 AGPL-3.0 许可证(网络服务传染性),以及 MaaFramework (LGPL-3.0)、Ultralytics YOLO (AGPL-3.0)、ONNX Runtime (MIT) 等第三方依赖的合规要求; +> +> **(3)法律风险认知**:我明确知晓使用 AAM **必然违反**《鹰角网络游戏使用许可及服务协议》第 6.2 条第 16 款(禁止未经授权的第三方软件),可能导致账号从警告到永久封禁的处罚,并理解该工具若用于含 PvP 的游戏将立即构成"AI 外挂"; +> +> **(4)YOLO 与 AI 风险自担**:我自愿承担 YOLO 模型误检(如漏检领袖单位、敌人分类错误)、卫戍协议 70 秒决策超时、以及 L5 层 AI 决策失误的全部风险,不会因任何损失向 Ethernos Studio 或 dhjs0000 提出索赔; +> +> **(5)停止使用承诺**:我承诺在《明日方舟》引入 PvP 模式或明确将视觉自动化(YOLO/ADB)纳入反作弊检测范围时,立即永久停止使用本软件。"** --- -## 8. 其他条款 +## 第九章 扩展免责声明与风险隔离条款 + +### 9.1 第三方平台与分发渠道免责 + +**GitHub/Microsoft 平台免责**: +AAM 源代码托管于 GitHub 平台(服务提供商:Microsoft Corporation)。您明确理解并同意: +- Ethernos Studio **无法控制** GitHub 平台的服务可用性、内容审核政策或账号封禁行为; +- 若 GitHub 因任何原因(包括但不限于鹰角网络或其关联方的投诉)删除 AAM 仓库、限制访问或封禁开发者账号,Ethernos Studio **不承担**因此导致的源代码获取权丧失、协作中断或社区支持终止的责任; +- 您应在本地保留 AAM 源代码副本,并关注 GitHub Releases 页面的镜像备份信息。 + +**包管理器与构建工具免责**: +AAM 依赖 vcpkg(Microsoft)、pip(Python Software Foundation)、Poetry 等第三方包管理器获取依赖库。Ethernos Studio **不对**以下情况承担责任: +- 包管理器服务器不可用、依赖版本冲突或供应链攻击(如恶意包注入); +- vcpkg 端口定义变更导致的构建失败; +- 依赖库(如 OpenCV、FFmpeg)发布的安全补丁与 AAM 代码的兼容性问题。 + +**应用商店与分发平台免责**: +若 AAM 的二进制分发版本通过任何第三方平台(如网盘、论坛、社交媒体)传播,Ethernos Studio **不保证**该等分发版本的完整性、安全性或未被篡改。建议仅从官方 GitHub Releases 页面获取经过 GPG 签名的发布包。 + +### 9.2 社区贡献与第三方代码免责 + +**Pull Request 与社区贡献**: +AAM 接受社区贡献(Pull Request、Issue 报告、代码补丁)。您明确同意: +- 社区贡献者(非 Ethernos Studio 核心维护者)提交的代码**未经**同等严格的审计与安全审查; +- 合并至主分支的社区贡献代码若存在安全漏洞、性能缺陷或法律风险(如意外引入 GPL 不兼容的依赖),Ethernos Studio **在发现后会尽力修复,但不承担因此导致的损失**; +- 您使用包含社区贡献代码的 AAM 版本时,应自行评估风险。 + +**第三方插件与扩展**: +AAM 的架构设计允许第三方开发插件(如自定义 YOLO 模型、L5 层策略模块)。Ethernos Studio **不对**以下情况承担责任: +- 第三方插件的安全漏洞、恶意代码或数据窃取行为; +- 第三方插件违反 AGPL-3.0 许可证导致的法律风险; +- 第三方插件与 AAM 核心版本的兼容性问题。 + +**Issue 与讨论区的信息风险**: +GitHub Issues、Discussions 及社区聊天群组(QQ、Discord 等)中的信息(包括使用教程、配置文件示例、YOLO 模型下载链接)由社区成员发布,**未经 Ethernos Studio 审核**。您应自行判断该等信息的准确性与安全性,**不应**将未知来源的配置文件或模型文件直接应用于生产环境。 + +### 9.3 技术演进与废弃声明 + +**技术栈变更风险**: +AAM 的技术栈(特别是 L1-YOLO 层)可能随计算机视觉领域的发展而演进(如从 YOLOv8 迁移至 YOLOv10、引入 Transformer-based 检测器)。您理解并同意: +- 旧版本 YOLO 模型可能因技术迭代而被标记为废弃(Deprecated),不再接收训练数据更新; +- 技术栈重大变更可能导致配置文件格式不兼容,您需要手动迁移配置; +- Ethernos Studio **无义务**无限期维护旧版本的技术支持。 + +**游戏引擎升级导致的失效**: +若《明日方舟》升级游戏引擎、更换渲染管线或修改 UI 设计风格,可能导致: +- YOLO 训练数据集失效,检测精度急剧下降; +- ADB 截图协议变更或屏幕坐标系变化; +- L3 层基于固定坐标的战术模板全部失效。 +Ethernos Studio **不保证**能够快速适配游戏重大更新,**不承诺**在特定时间内恢复软件功能。 + +**维护者变更与项目终止**: +Ethernos Studio 保留在任何时候以任何理由(包括但不限于开发者精力有限、法律风险增加、失去对《明日方舟》游戏的兴趣)**永久终止 AAM 项目**的权利。项目终止可能表现为: +- 仓库归档(Archive),不再接受新功能开发; +- 停止响应 Issues 与 Pull Requests; +- 删除或隐藏历史发布版本。 +项目终止后,AGPL-3.0 许可证仍然有效,社区可继续 fork 维护,但 Ethernos Studio **不再承担任何维护义务**。 + +### 9.4 跨境法律与出口管制合规 + +**出口管制合规声明**: +AAM 包含可能受出口管制法规约束的技术组件(如加密算法、GPU 加速库)。您确认: +- 若您位于或受制裁国家/地区(如美国 OFAC 制裁清单),使用 AAM 可能违反当地法律; +- 您不会将 AAM 源代码或二进制文件传输至受制裁实体或个人; +- Ethernos Studio **不对**因出口管制法规导致的软件使用限制承担责任。 + +**跨境数据传输**: +若您启用 L5 层 AI 决策功能并配置非中国大陆地区的 LLM API(如 OpenAI、Anthropic),游戏状态数据将跨境传输至境外服务器。您应自行确保该等数据传输符合《中华人民共和国数据安全法》《个人信息保护法》等法规要求,Ethernos Studio **不提供**合规性保证。 + +**司法管辖区冲突**: +若您位于中华人民共和国大陆地区之外,使用 AAM 可能同时受当地法律(如欧盟 GDPR、美国 DMCA)约束。Ethernos Studio **不保证** AAM 符合所有司法管辖区的法律要求,您应自行评估当地法律风险。 + +### 9.5 不可抗力与外部事件 + +**不可抗力事件**: +Ethernos Studio **不对**因不可抗力事件导致的软件无法使用或损失承担责任,包括但不限于: +- 自然灾害(地震、洪水、火灾)导致的数据中心损毁; +- 战争、武装冲突、恐怖袭击或社会动乱; +- 政府行为、法律法规变更或行政命令(如游戏行业监管政策收紧); +- 大规模网络攻击(DDoS、勒索软件)导致的服务中断; +- 全球流行病或公共卫生事件导致的开发停滞。 + +**游戏运营商行为**: +您明确理解并同意:**鹰角网络作为游戏运营商,拥有绝对的技术与合同权力封禁使用 AAM 的账号**。Ethernos Studio **无法**也**不会**尝试通过法律手段干预鹰角网络的账号管理决策。若您的账号被封禁: +- 您**无权**要求 Ethernos Studio 提供法律援助或申诉代理; +- 您**不应**以 Ethernos Studio 为被告提起任何诉讼; +- 您与鹰角网络之间的争议**完全独立于**本协议。 + +**反作弊技术升级**: +若鹰角网络引入新的反作弊技术(如基于 AI 的行为检测、硬件指纹识别、内核级驱动保护),导致 AAM 被检测或设备被标记,Ethernos Studio **不保证**能够开发规避措施,**不承担**因此导致的设备封禁或硬件黑名单损失。 + +### 9.6 集体诉讼与代表诉讼放弃 + +**集体诉讼放弃(Class Action Waiver)**: +在适用法律允许的最大范围内,您同意**仅以个人名义**对 Ethernos Studio 提出索赔,不得作为原告或集体成员参与任何集体诉讼、代表诉讼、集团仲裁或私人检察长诉讼。您明确放弃任何通过集体诉讼获得救济的权利。 + +**争议解决前置程序**: +若您与 Ethernos Studio 发生争议,您同意在提起正式诉讼或仲裁前,首先通过以下方式尝试解决: +1. 通过 GitHub Issues 提交书面投诉,描述争议事实与诉求; +2. 给予 Ethernos Studio 不少于 30 个自然日的回应与协商期; +3. 若协商未果,争议应提交**长沙仲裁委员会**按照其届时有效的仲裁规则进行仲裁,仲裁地点为**湖南省长沙市**,仲裁语言为**中文**。 + +**仲裁裁决的终局性**: +您同意仲裁裁决是终局的,对双方均有约束力,可在任何有管辖权的法院申请强制执行。 + +### 9.7 可分割性条款 -8.1 **独立性**:若本协议某条款被有权机关认定为无效,不影响其他条款效力。 +若本协议任何条款被有管辖权的法院或仲裁机构认定为无效、不可执行或违法: +- 该条款应在**最小必要范围内**被修改以使其有效且可执行,或若无法修改则予以删除; +- 该条款的无效性**不影响**本协议其他条款的有效性与可执行性; +- 双方应本着诚信原则协商替代条款,以尽可能接近原条款的经济与法律效果。 -8.2 **非代理关系**:本协议不构成您与 Ethernos Studio 之间的合伙、合资、雇佣或代理关系。 +### 9.8 完整协议条款 -8.3 **联系信息**: -- 项目仓库:https://github.com/Ethernos-Studio/ArknightsAutoMachine -- 官方邮箱:support@ethernos.net -- 技术交流群:1070963958(QQ) +本协议(包括所有引用的第三方许可证文件)构成您与 Ethernos Studio 之间关于 AAM 软件的**完整协议**,取代双方此前或同时就本协议主题达成的所有口头或书面谅解、协议、陈述或保证。 + +您确认:在点击"接受"按钮或使用 AAM 软件之前,您已获得充分的机会阅读、理解并咨询法律顾问关于本协议全部条款的意见。您**不因**以下理由主张本协议无效或可撤销: +- 未阅读或理解协议内容; +- 未获得独立的法律建议; +- 协议条款不公平或不合理。 + +### 9.9 权利放弃与不弃权声明 + +Ethernos Studio **未行使**或**延迟行使**本协议项下的任何权利、权力或救济,**不构成**对该等权利、权力或救济的放弃,也不妨碍日后行使该等权利、权力或救济。 + +单次或部分行使任何权利、权力或救济**不妨碍**进一步行使该等或任何其他权利、权力或救济。 + +Ethernos Studio 对您违反本协议行为的弃权**不构成**对后续相同或类似违约行为的弃权。 + +### 9.10 转让限制 + +未经 Ethernos Studio 事先书面同意,您不得转让、转移或分包本协议项下的任何权利或义务。任何未经同意的转让均为无效。 + +Ethernos Studio 可**无需您同意**即转让本协议项下的全部或部分权利义务给关联公司或在合并、收购或资产出售中转让给继任者。 + +### 9.11 关系界定 + +本协议**不构成**您与 Ethernos Studio 之间建立以下关系: +- 合伙或合资企业; +- 雇佣或劳动关系; +- 特许经营关系; +- 代理或委托关系。 + +双方均为独立缔约方,本协议**不产生**任何一方代表另一方行事的权利或授权。 + +### 9.12 语言与解释 + +**语言优先性**: +本协议以**简体中文**起草。若本协议被翻译为其他语言,简体中文版本与翻译版本存在冲突时,**以简体中文版本为准**。 + +**标题与解释**: +本协议中的标题仅为方便阅读而设,**不影响**本协议的解释。本协议中的"包括"、"包含"等词语应理解为"包括但不限于"。 + +--- + +## 第十章 最终用户确认 + +**强制性确认按钮文案**(用户必须滚动阅读全文后方可点击): + +**"我已完整阅读、充分理解并接受《Arknights Auto Machine 综合用户协议》(v2.1.0-YOLO-Enhanced)的全部条款,包括:** + + - [] **(1)技术架构理解**:我确认理解 AAM 的 L0-L5 分层架构,特别是 **L1-YOLO 层** 使用 Ultralytics YOLO 模型进行实时目标检测的技术边界与误检风险;我理解对于「卫戍协议」模式,YOLO 仅提供**反应式视觉辅助**,**无法预测**随机地图布局或处理多人协同策略; + + - [] **(2)许可证合规义务**:我承诺遵守 AGPL-3.0 许可证(网络服务传染性),以及 MaaFramework (LGPL-3.0)、Ultralytics YOLO (AGPL-3.0)、ONNX Runtime (MIT) 等第三方依赖的合规要求;我理解若基于 AAM 提供网络服务,必须向所有用户公开服务端源代码; + + - [] **(3)法律风险认知**:我明确知晓使用 AAM **必然违反**《鹰角网络游戏使用许可及服务协议》第 6.2 条第 16 款,可能导致账号从警告到永久封禁的处罚;我理解该工具若用于含 PvP 的游戏将立即构成"AI 外挂";我承诺在《明日方舟》引入 PvP 模式或明确将视觉自动化纳入反作弊检测范围时,立即永久停止使用本软件; + + - [] **(4)YOLO 与 AI 风险自担**:我自愿承担 YOLO 模型误检(如漏检领袖单位、敌人分类错误)、卫戍协议 70 秒决策超时、L5 层 AI 决策失误的全部风险;我理解 AAM **不保证**通关任何特定关卡,**不保证**胜率高于人工操作; + + - [] **(5)第三方依赖风险**:我理解 AAM 依赖 GitHub、vcpkg、第三方 AI API 等外部服务,Ethernos Studio **不保证**该等服务的持续可用性;我理解社区贡献代码、第三方插件**未经**同等安全审计,使用风险自担; + + - [] **(6)项目终止风险**:我理解 Ethernos Studio 可能因任何原因(法律风险、精力有限、失去兴趣)**永久终止 AAM 项目**,届时将不再提供技术支持,但我保留 AGPL-3.0 授予的开源权利; + + - [] **(7)争议解决方式**:我理解任何争议应首先通过 GitHub Issues 协商解决,协商未果应提交**长沙仲裁委员会**仲裁,仲裁地点为湖南省长沙市,我放弃集体诉讼权利; + + - [] **(8)责任限制确认**:我理解 Ethernos Studio 的责任上限为 **人民币 50 元或我实际支付金额(通常为 0 元)**;我同意不会因任何损失(包括但不限于账号封禁、虚拟财产损失、硬件损坏)向 Ethernos Studio 或 dhjs0000 提出索赔、诉讼或仲裁; + + - [] **(9)完整协议确认**:我确认本协议构成我与 Ethernos Studio 之间的完整协议,我已获得充分机会阅读、理解并咨询法律顾问,我不以"未阅读"或"不理解"为由主张协议无效。" --- -**确认按钮文案建议**(供 GUI 使用): +**协议结束** + +--- -> **"我已阅读并理解《AAM 用户协议》。我知晓使用本工具可能导致明日方舟账号被封禁,并自愿承担全部风险。我同意遵守 AGPL-3.0 开源许可证义务。"** +*本协议最后更新日期:2026-04-07* +*版本:v2.2.0* +*起草与维护:Ethernos Studio* \ No newline at end of file diff --git a/core/include/aam/l0/shm_transport.hpp b/core/include/aam/l0/shm_transport.hpp new file mode 100644 index 0000000..fdd6b3e --- /dev/null +++ b/core/include/aam/l0/shm_transport.hpp @@ -0,0 +1,990 @@ +// ========================================================================== +// Copyright (C) 2026 Ethernos Studio +// This file is part of Arknights Auto Machine (AAM). +// +// AAM is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AAM is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with AAM. If not, see . +// ========================================================================== +// @file shm_transport.hpp +// @author dhjs0000 +// @brief L0 共享内存传输层实现 +// ========================================================================== +// 版本: v0.2.0-alpha.3 +// 功能: 零拷贝共享内存传输,支持 Windows 命名共享内存和 POSIX shm +// 依赖: C++23, Windows API / POSIX shm_open +// 算法: 环形缓冲区 + 原子序列号 + 内存屏障 +// 性能: 单帧传输延迟 < 1μs,吞吐量 > 10GB/s +// ========================================================================== + +#ifndef AAM_L0_SHM_TRANSPORT_HPP +#define AAM_L0_SHM_TRANSPORT_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "aam/core/timer.hpp" +#include "aam/l0/capture_backend.hpp" + +// 禁用 MSVC 结构填充警告 +#ifdef _WIN32 +# pragma warning(push) +# pragma warning(disable : 4324) // 禁用"结构被填充"警告 +#endif + +// 平台特定头文件 +#ifdef _WIN32 +# ifndef NOMINMAX +# define NOMINMAX +# endif +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# include +#else +# include +# include +# include +# include +#endif + +namespace aam::l0 +{ + +// ========================================================================== +// 前向声明 +// ========================================================================== +class SharedMemorySegment; +struct ShmTransportConfig; +struct ShmTransportStats; + +// ========================================================================== +// 错误码定义 +// ========================================================================== + +/** + * @brief 共享内存传输错误码 + * @details 使用强类型枚举确保类型安全 + */ +enum class ShmTransportError : std::uint32_t +{ + // 通用错误 (0x0000 - 0x00FF) + Success = 0, ///< 操作成功 + Unknown = 0x0001, ///< 未知错误 + InvalidArgument = 0x0002, ///< 无效参数 + OutOfMemory = 0x0003, ///< 内存不足 + Timeout = 0x0004, ///< 操作超时 + NotInitialized = 0x0005, ///< 未初始化 + AlreadyExists = 0x0006, ///< 资源已存在 + NotFound = 0x0007, ///< 资源未找到 + PermissionDenied = 0x0008, ///< 权限不足 + + // 共享内存错误 (0x0100 - 0x01FF) + ShmCreateFailed = 0x0100, ///< 创建共享内存失败 + ShmOpenFailed = 0x0101, ///< 打开共享内存失败 + ShmMapFailed = 0x0102, ///< 映射共享内存失败 + ShmUnmapFailed = 0x0103, ///< 解除映射失败 + ShmTooSmall = 0x0104, ///< 共享内存空间不足 + ShmCorrupted = 0x0105, ///< 共享内存数据损坏 + ShmVersionMismatch = 0x0106, ///< 版本不匹配 + + // 同步错误 (0x0200 - 0x02FF) + SyncCreateFailed = 0x0200, ///< 创建同步对象失败 + SyncWaitFailed = 0x0201, ///< 等待同步对象失败 + SyncSignalFailed = 0x0202, ///< 信号通知失败 + SyncTimeout = 0x0203, ///< 同步超时 + SyncAbandoned = 0x0204, ///< 同步对象被放弃 + + // 传输错误 (0x0300 - 0x03FF) + TransportClosed = 0x0300, ///< 传输已关闭 + TransportBusy = 0x0301, ///< 传输忙 + BufferOverflow = 0x0302, ///< 缓冲区溢出 + BufferUnderflow = 0x0303, ///< 缓冲区下溢 + FrameTooLarge = 0x0304, ///< 帧数据过大 + InvalidFrame = 0x0305, ///< 无效帧数据 +}; + +/** + * @brief 获取 ShmTransportError 的错误类别 + * @return 错误类别引用 + */ +[[nodiscard]] const std::error_category& shm_transport_error_category() noexcept; + +/** + * @brief 创建 std::error_code + * @param e 错误码 + * @return error_code 对象 + */ +[[nodiscard]] inline std::error_code make_error_code(ShmTransportError e) noexcept +{ + return {static_cast(e), shm_transport_error_category()}; +} + +} // namespace aam::l0 + +// ========================================================================== +// std::error_code 特化 +// ========================================================================== +template <> +struct std::is_error_code_enum : std::true_type +{ +}; + +namespace aam::l0 +{ + +// ========================================================================== +// 共享内存控制结构(头部)- 必须在 ShmTransportConfig 之前定义 +// ========================================================================== + +// 帧头部大小常量 +constexpr std::size_t SHM_FRAME_HEADER_SIZE = 64; // alignas(64) +constexpr std::size_t SHM_CONTROL_BLOCK_SIZE = 512; // 估算值,实际使用 sizeof + +/** + * @brief 共享内存帧头部 + * @details 每帧数据的元数据,位于帧数据之前 + */ +struct alignas(64) ShmFrameHeader +{ + static constexpr std::uint32_t MAGIC = 0x41414D46; ///< "AAMF" + + std::uint32_t magic{MAGIC}; ///< 魔数,用于验证 + std::uint32_t version{1}; ///< 结构版本 + std::uint32_t sequence_number{0}; ///< 帧序列号 + std::uint32_t frame_number{0}; ///< 帧编号 + std::uint32_t sequence_id{0}; ///< 序列ID + std::uint32_t data_size{0}; ///< 实际数据大小 + + // 时间戳 + std::uint64_t capture_timestamp_ns{0}; ///< 捕获时间戳(纳秒) + std::uint64_t write_timestamp_ns{0}; ///< 写入时间戳(纳秒) + + // 帧元数据 + std::uint32_t width{0}; ///< 图像宽度 + std::uint32_t height{0}; ///< 图像高度 + std::uint32_t stride{0}; ///< 行步长 + std::uint32_t pixel_format{0}; ///< 像素格式(PixelFormat 枚举值) + + // 校验 + std::uint32_t checksum{0}; ///< 数据校验和(CRC32) + std::uint32_t reserved{0}; ///< 保留字段(对齐) + + /** + * @brief 验证头部有效性 + * @return true 如果头部有效 + */ + [[nodiscard]] bool is_valid() const noexcept + { + return magic == MAGIC && version == 1 && data_size > 0; + } + + /** + * @brief 计算校验和 + * @param data 帧数据指针 + * @return CRC32 校验和 + */ + [[nodiscard]] std::uint32_t calculate_checksum(const std::byte* data) const noexcept; + + /** + * @brief 验证数据完整性 + * @param data 帧数据指针 + * @return true 如果数据完整 + */ + [[nodiscard]] bool verify_data(const std::byte* data) const noexcept; +}; + +/** + * @brief 共享内存控制块 + * @details 位于共享内存起始位置,管理整个传输状态 + * @note 所有原子操作使用 acquire-release 语义保证跨进程可见性 + */ +struct alignas(64) ShmControlBlock +{ + static constexpr std::uint32_t MAGIC = 0x41414D53; ///< "AAMS" + static constexpr std::uint32_t VERSION = 1; + + // 魔数和版本(只读,创建时初始化) + std::uint32_t magic{MAGIC}; ///< 魔数 + std::uint32_t version{VERSION}; ///< 协议版本 + std::uint32_t header_size{0}; ///< 控制块大小 + std::uint32_t flags{0}; ///< 标志位 + + // 缓冲区配置(只读,创建时初始化) + std::uint32_t buffer_count{0}; ///< 缓冲区数量 + std::uint32_t max_frame_size{0}; ///< 最大帧大小 + std::uint32_t metadata_size{0}; ///< 元数据区域大小 + std::uint32_t frame_stride{0}; ///< 每帧对齐后的大小 + + // 序列号(原子操作) + alignas(64) std::atomic write_sequence{0}; ///< 写入序列号 + alignas(64) std::atomic read_sequence{0}; ///< 读取序列号 + alignas(64) std::atomic dropped_frames{0}; ///< 丢弃帧计数 + + // 状态标志(原子操作) + alignas(64) std::atomic state{0}; ///< 传输状态 + alignas(64) std::atomic active_readers{0}; ///< 活跃读取者数 + alignas(64) std::atomic active_writers{0}; ///< 活跃写入者数 + + // 统计信息(原子操作) + alignas(64) std::atomic total_frames_written{0}; ///< 总写入帧数 + alignas(64) std::atomic total_frames_read{0}; ///< 总读取帧数 + alignas(64) std::atomic total_bytes_written{0}; ///< 总写入字节数 + alignas(64) std::atomic total_bytes_read{0}; ///< 总读取字节数 + + // 状态常量 + static constexpr std::uint32_t STATE_INITIALIZED = 0x01; ///< 已初始化 + static constexpr std::uint32_t STATE_ACTIVE = 0x02; ///< 活跃状态 + static constexpr std::uint32_t STATE_SHUTDOWN = 0x04; ///< 已关闭 + static constexpr std::uint32_t STATE_ERROR = 0x08; ///< 错误状态 + + /** + * @brief 验证控制块有效性 + * @return true 如果控制块有效 + */ + [[nodiscard]] bool is_valid() const noexcept + { + return magic == MAGIC && version == VERSION; + } + + /** + * @brief 获取当前写入索引 + * @return 写入缓冲区索引 + */ + [[nodiscard]] std::uint32_t get_write_index() const noexcept + { + return static_cast(write_sequence.load(std::memory_order_relaxed) % buffer_count); + } + + /** + * @brief 获取当前读取索引 + * @return 读取缓冲区索引 + */ + [[nodiscard]] std::uint32_t get_read_index() const noexcept + { + return static_cast(read_sequence.load(std::memory_order_relaxed) % buffer_count); + } + + /** + * @brief 检查是否有可读数据 + * @return true 如果有未读数据 + */ + [[nodiscard]] bool has_readable_data() const noexcept + { + return read_sequence.load(std::memory_order_acquire) < + write_sequence.load(std::memory_order_acquire); + } + + /** + * @brief 检查是否可写入 + * @return true 如果有可用缓冲区 + */ + [[nodiscard]] bool is_writable() const noexcept + { + const std::uint64_t write_seq = write_sequence.load(std::memory_order_relaxed); + const std::uint64_t read_seq = read_sequence.load(std::memory_order_acquire); + return (write_seq - read_seq) < buffer_count; + } + + /** + * @brief 获取可用缓冲区数量 + * @return 可用缓冲区数量 + */ + [[nodiscard]] std::uint32_t available_buffers() const noexcept + { + const std::uint64_t write_seq = write_sequence.load(std::memory_order_relaxed); + const std::uint64_t read_seq = read_sequence.load(std::memory_order_acquire); + return static_cast(buffer_count - (write_seq - read_seq)); + } + + /** + * @brief 获取已用缓冲区数量 + * @return 已用缓冲区数量 + */ + [[nodiscard]] std::uint32_t used_buffers() const noexcept + { + const std::uint64_t write_seq = write_sequence.load(std::memory_order_acquire); + const std::uint64_t read_seq = read_sequence.load(std::memory_order_relaxed); + return static_cast(write_seq - read_seq); + } +}; + +// ========================================================================== +// 共享内存传输配置 +// ========================================================================== + +/** + * @brief 共享内存传输配置 + * @details 定义共享内存传输的行为参数 + */ +struct ShmTransportConfig +{ + // 共享内存名称(平台特定前缀自动添加) + std::string shm_name{"aam_frame_buffer"}; ///< 共享内存对象名称 + + // 缓冲区配置 + std::size_t buffer_count{4}; ///< 帧缓冲区数量(环形) + std::size_t max_frame_size{1920 * 1080 * 4}; ///< 最大帧大小(字节) + std::size_t metadata_size{4096}; ///< 元数据区域大小(字节) + + // 同步配置 + core::Duration write_timeout{std::chrono::milliseconds(100)}; ///< 写入超时 + core::Duration read_timeout{std::chrono::milliseconds(100)}; ///< 读取超时 + bool non_blocking{false}; ///< 非阻塞模式 + + // 性能配置 + bool use_cache_line_alignment{true}; ///< 使用缓存行对齐(避免伪共享) + bool prefetch_next_frame{true}; ///< 预取下一帧数据 + bool enable_zero_copy{true}; ///< 启用零拷贝模式(跳过CRC校验) + bool enable_checksum{false}; ///< 启用CRC校验(默认关闭以提高性能) + + // 调试配置 + bool enable_stats{true}; ///< 启用统计信息 + bool enable_tracing{false}; ///< 启用详细跟踪日志 + + /** + * @brief 计算所需共享内存总大小 + * @return 总大小(字节) + * @complexity O(1) + */ + [[nodiscard]] constexpr std::size_t calculate_total_size() const noexcept + { + // 头部:控制结构 + 元数据 + const std::size_t header_size = sizeof(ShmControlBlock) + metadata_size; + + // 帧缓冲区:每个帧需要头部 + 数据,对齐到缓存行 + constexpr std::size_t cache_line = 64; + const std::size_t frame_header_size = sizeof(ShmFrameHeader); + const std::size_t aligned_frame_size = + ((frame_header_size + max_frame_size + cache_line - 1) / cache_line) * cache_line; + + return header_size + (aligned_frame_size * buffer_count); + } + + /** + * @brief 验证配置有效性 + * @return 错误码或 void + */ + [[nodiscard]] std::expected validate() const noexcept + { + if (shm_name.empty()) { + return std::unexpected(ShmTransportError::InvalidArgument); + } + if (buffer_count == 0 || buffer_count > 64) { + return std::unexpected(ShmTransportError::InvalidArgument); + } + if (max_frame_size == 0 || max_frame_size > (256 * 1024 * 1024)) { + return std::unexpected(ShmTransportError::InvalidArgument); + } + return {}; + } +}; + +// ========================================================================== +// 共享内存段(平台抽象) +// ========================================================================== + +/** + * @brief 共享内存段抽象类 + * @details 封装平台特定的共享内存操作 + */ +class SharedMemorySegment +{ +public: + // ================================================================== + // 类型别名 + // ================================================================== + using Result = std::expected; + + // ================================================================== + // 构造与析构 + // ================================================================== + SharedMemorySegment() noexcept = default; + virtual ~SharedMemorySegment() = default; + + // 禁用拷贝 + SharedMemorySegment(const SharedMemorySegment&) = delete; + SharedMemorySegment& operator=(const SharedMemorySegment&) = delete; + + // 允许移动 + SharedMemorySegment(SharedMemorySegment&&) = default; + SharedMemorySegment& operator=(SharedMemorySegment&&) = default; + + // ================================================================== + // 工厂方法 + // ================================================================== + + /** + * @brief 创建共享内存段(生产者) + * @param name 共享内存名称 + * @param size 共享内存大小 + * @return 成功返回 SharedMemorySegment 实例,失败返回错误码 + * @complexity O(1),系统调用 + */ + [[nodiscard]] static std::expected, ShmTransportError> + Create(std::string_view name, std::size_t size); + + /** + * @brief 打开现有共享内存段(消费者) + * @param name 共享内存名称 + * @param size 共享内存大小(用于验证) + * @return 成功返回 SharedMemorySegment 实例,失败返回错误码 + * @complexity O(1),系统调用 + */ + [[nodiscard]] static std::expected, ShmTransportError> + Open(std::string_view name, std::size_t size); + + // ================================================================== + // 访问接口 + // ================================================================== + + /** + * @brief 获取共享内存基地址 + * @return 共享内存起始地址 + */ + [[nodiscard]] virtual std::byte* data() const noexcept = 0; + + /** + * @brief 获取共享内存大小 + * @return 共享内存大小(字节) + */ + [[nodiscard]] virtual std::size_t size() const noexcept = 0; + + /** + * @brief 获取共享内存名称 + * @return 共享内存名称 + */ + [[nodiscard]] virtual std::string_view name() const noexcept = 0; + + /** + * @brief 检查是否有效 + * @return true 如果共享内存有效映射 + */ + [[nodiscard]] virtual bool is_valid() const noexcept = 0; + + /** + * @brief 刷新共享内存到磁盘(如果需要) + * @return 操作结果 + */ + [[nodiscard]] virtual Result flush() noexcept = 0; +}; + +// ========================================================================== +// 共享内存传输统计 +// ========================================================================== + +/** + * @brief 共享内存传输统计信息 + * @note 使用原子类型保证线程安全,避免锁竞争 + */ +struct ShmTransportStats +{ + // 帧统计(受 stats_mutex_ 保护) + std::uint64_t frames_written{0}; ///< 已写入帧数 + std::uint64_t frames_read{0}; ///< 已读取帧数 + std::uint64_t frames_dropped{0}; ///< 丢弃帧数 + + // 字节统计(受 stats_mutex_ 保护) + std::uint64_t bytes_written{0}; ///< 已写入字节数 + std::uint64_t bytes_read{0}; ///< 已读取字节数 + + // 延迟统计(纳秒,受 stats_mutex_ 保护) + core::Duration min_write_latency{core::Duration::max()}; ///< 最小写入延迟 + core::Duration max_write_latency{core::Duration::zero()}; ///< 最大写入延迟 + core::Duration avg_write_latency{core::Duration::zero()}; ///< 平均写入延迟 + + core::Duration min_read_latency{core::Duration::max()}; ///< 最小读取延迟 + core::Duration max_read_latency{core::Duration::zero()}; ///< 最大读取延迟 + core::Duration avg_read_latency{core::Duration::zero()}; ///< 平均读取延迟 + + // 传输统计(原子类型,无锁更新) + std::atomic write_timeouts{0}; ///< 写入超时次数 + std::atomic read_timeouts{0}; ///< 读取超时次数 + std::atomic checksum_errors{0}; ///< 校验和错误次数 + + // 时间戳(受 stats_mutex_ 保护) + core::Timestamp session_start; ///< 会话开始时间 + core::Timestamp last_write_time; ///< 最后写入时间 + core::Timestamp last_read_time; ///< 最后读取时间 + + // 显式定义拷贝构造函数(处理原子成员) + ShmTransportStats() = default; + + ShmTransportStats(const ShmTransportStats& other) + : frames_written(other.frames_written) + , frames_read(other.frames_read) + , frames_dropped(other.frames_dropped) + , bytes_written(other.bytes_written) + , bytes_read(other.bytes_read) + , min_write_latency(other.min_write_latency) + , max_write_latency(other.max_write_latency) + , avg_write_latency(other.avg_write_latency) + , min_read_latency(other.min_read_latency) + , max_read_latency(other.max_read_latency) + , avg_read_latency(other.avg_read_latency) + , write_timeouts(other.write_timeouts.load(std::memory_order_relaxed)) + , read_timeouts(other.read_timeouts.load(std::memory_order_relaxed)) + , checksum_errors(other.checksum_errors.load(std::memory_order_relaxed)) + , session_start(other.session_start) + , last_write_time(other.last_write_time) + , last_read_time(other.last_read_time) + {} + + ShmTransportStats& operator=(const ShmTransportStats& other) + { + if (this != &other) { + frames_written = other.frames_written; + frames_read = other.frames_read; + frames_dropped = other.frames_dropped; + bytes_written = other.bytes_written; + bytes_read = other.bytes_read; + min_write_latency = other.min_write_latency; + max_write_latency = other.max_write_latency; + avg_write_latency = other.avg_write_latency; + min_read_latency = other.min_read_latency; + max_read_latency = other.max_read_latency; + avg_read_latency = other.avg_read_latency; + write_timeouts.store(other.write_timeouts.load(std::memory_order_relaxed), std::memory_order_relaxed); + read_timeouts.store(other.read_timeouts.load(std::memory_order_relaxed), std::memory_order_relaxed); + checksum_errors.store(other.checksum_errors.load(std::memory_order_relaxed), std::memory_order_relaxed); + session_start = other.session_start; + last_write_time = other.last_write_time; + last_read_time = other.last_read_time; + } + return *this; + } + + /** + * @brief 计算丢帧率 + * @return 丢帧率 [0.0, 1.0] + */ + [[nodiscard]] double get_drop_rate() const noexcept + { + const std::uint64_t total = frames_written + frames_dropped; + return total > 0 ? static_cast(frames_dropped) / total : 0.0; + } + + /** + * @brief 计算平均帧大小 + * @return 平均帧大小(字节) + */ + [[nodiscard]] std::uint64_t get_average_frame_size() const noexcept + { + return frames_written > 0 ? bytes_written / frames_written : 0; + } + + /** + * @brief 计算吞吐量(字节/秒) + * @return 吞吐量估算 + */ + [[nodiscard]] double get_throughput_bytes_per_sec() const noexcept + { + const auto duration = core::Clock::now() - session_start; + const auto duration_sec = std::chrono::duration(duration).count(); + return duration_sec > 0 ? static_cast(bytes_written) / duration_sec : 0.0; + } + + /** + * @brief 更新写入延迟统计 + * @param latency 写入延迟 + */ + void update_write_latency(core::Duration latency) noexcept + { + min_write_latency = std::min(min_write_latency, latency); + max_write_latency = std::max(max_write_latency, latency); + // 指数移动平均 + const double alpha = 0.1; + const double new_ns = static_cast(latency.count()); + const double old_ns = static_cast(avg_write_latency.count()); + avg_write_latency = core::Duration(static_cast( + alpha * new_ns + (1.0 - alpha) * old_ns)); + } + + /** + * @brief 更新读取延迟统计 + * @param latency 读取延迟 + */ + void update_read_latency(core::Duration latency) noexcept + { + min_read_latency = std::min(min_read_latency, latency); + max_read_latency = std::max(max_read_latency, latency); + // 指数移动平均 + const double alpha = 0.1; + const double new_ns = static_cast(latency.count()); + const double old_ns = static_cast(avg_read_latency.count()); + avg_read_latency = core::Duration(static_cast( + alpha * new_ns + (1.0 - alpha) * old_ns)); + } + + /** + * @brief 重置统计信息 + */ + void reset() noexcept + { + *this = ShmTransportStats{}; + session_start = core::Clock::now(); + } +}; + +// ========================================================================== +// 共享内存传输器 +// ========================================================================== + +/** + * @brief 共享内存传输器 + * @details 提供零拷贝的帧数据传输,支持单生产者-多消费者模式 + * @note 线程安全:写入操作是单生产者安全的,读取操作是多消费者安全的 + * @warning 消费者必须在生产者初始化后才能连接 + */ +class ShmTransport +{ +public: + // ================================================================== + // 类型别名 + // ================================================================== + using Result = std::expected; + + // ================================================================== + // 构造与析构 + // ================================================================== + + /** + * @brief 默认构造函数 + * @complexity O(1) + */ + ShmTransport() noexcept = default; + + /** + * @brief 析构函数 + * @complexity O(1),自动关闭传输 + */ + ~ShmTransport() noexcept; + + // 禁用拷贝 + ShmTransport(const ShmTransport&) = delete; + ShmTransport& operator=(const ShmTransport&) = delete; + + // 允许移动 + ShmTransport(ShmTransport&& other) noexcept; + ShmTransport& operator=(ShmTransport&& other) noexcept; + + // ================================================================== + // 生命周期管理(生产者) + // ================================================================== + + /** + * @brief 初始化传输器(作为生产者) + * @param config 传输配置 + * @return 成功返回 void,失败返回错误码 + * @complexity O(1),创建共享内存和同步对象 + * @thread_safety 线程安全,但应在启动消费者前调用 + */ + [[nodiscard]] Result InitializeProducer(const ShmTransportConfig& config); + + /** + * @brief 初始化传输器(作为消费者) + * @param config 传输配置 + * @return 成功返回 void,失败返回错误码 + * @complexity O(1),打开共享内存 + * @thread_safety 线程安全 + * @note 必须在生产者初始化后调用 + */ + [[nodiscard]] Result InitializeConsumer(const ShmTransportConfig& config); + + /** + * @brief 关闭传输器 + * @return 成功返回 void,失败返回错误码 + * @complexity O(1) + */ + [[nodiscard]] Result Shutdown() noexcept; + + /** + * @brief 检查是否已初始化 + * @return true 如果已初始化 + */ + [[nodiscard]] bool IsInitialized() const noexcept; + + /** + * @brief 检查是否为生产者模式 + * @return true 如果是生产者 + */ + [[nodiscard]] bool IsProducer() const noexcept; + + /** + * @brief 检查是否为消费者模式 + * @return true 如果是消费者 + */ + [[nodiscard]] bool IsConsumer() const noexcept; + + // ================================================================== + // 帧写入接口(生产者) + // ================================================================== + + /** + * @brief 写入帧数据 + * @param metadata 帧元数据 + * @param data 帧数据 + * @return 成功返回 void,失败返回错误码 + * @complexity O(n),n为数据大小(内存拷贝) + * @thread_safety 单生产者安全 + * @note 如果缓冲区满,根据配置可能阻塞或丢弃 + */ + [[nodiscard]] Result WriteFrame(const FrameMetadata& metadata, std::span data); + + /** + * @brief 尝试写入帧数据(非阻塞) + * @param metadata 帧元数据 + * @param data 帧数据 + * @return 成功返回 true,缓冲区满返回 false,错误返回错误码 + * @complexity O(n) + * @thread_safety 单生产者安全 + */ + [[nodiscard]] std::expected TryWriteFrame( + const FrameMetadata& metadata, std::span data); + + /** + * @brief 写入帧数据(带超时) + * @param metadata 帧元数据 + * @param data 帧数据 + * @param timeout 超时时间 + * @return 成功返回 true,超时返回 false,错误返回错误码 + * @complexity O(n) + * @thread_safety 单生产者安全 + */ + [[nodiscard]] std::expected WriteFrameWithTimeout( + const FrameMetadata& metadata, std::span data, core::Duration timeout); + + // ================================================================== + // 帧读取接口(消费者) + // ================================================================== + + /** + * @brief 读取帧数据 + * @param timeout 超时时间 + * @return 成功返回帧数据,超时返回 nullopt,错误返回错误码 + * @complexity O(n),n为数据大小(内存拷贝) + * @thread_safety 多消费者安全 + */ + [[nodiscard]] std::expected>>, ShmTransportError> + ReadFrame(core::Duration timeout); + + /** + * @brief 尝试读取帧数据(非阻塞) + * @return 成功返回帧数据,无数据返回 nullopt,错误返回错误码 + * @complexity O(n) + * @thread_safety 多消费者安全 + */ + [[nodiscard]] std::expected>>, ShmTransportError> + TryReadFrame(); + + /** + * @brief 读取帧数据(回调方式) + * @param timeout 超时时间 + * @param callback 数据回调函数 + * @return 成功返回 void,失败返回错误码 + * @complexity O(n) + * @thread_safety 多消费者安全 + * @note 回调方式避免额外内存分配 + */ + using ReadCallback = std::function)>; + [[nodiscard]] Result ReadFrameWithCallback(core::Duration timeout, ReadCallback callback); + + // ================================================================== + // 零拷贝帧接口(高性能) + // ================================================================== + + /** + * @brief 获取写入缓冲区(零拷贝) + * @param timeout 超时时间 + * @return 成功返回可写入的缓冲区指针和元数据,失败返回错误码 + * @complexity O(1),无内存拷贝 + * @thread_safety 单生产者安全 + * @note 调用者直接写入返回的缓冲区,然后通过 CommitWriteBuffer 提交 + */ + struct WriteBuffer { + std::byte* data{nullptr}; ///< 数据缓冲区指针 + std::size_t capacity{0}; ///< 缓冲区容量 + std::uint32_t buffer_index{0}; ///< 缓冲区索引(用于提交) + }; + [[nodiscard]] std::expected, ShmTransportError> + AcquireWriteBuffer(core::Duration timeout); + + /** + * @brief 提交写入的缓冲区 + * @param buffer_index AcquireWriteBuffer 返回的 buffer_index + * @param metadata 帧元数据 + * @param actual_size 实际写入的数据大小 + * @return 成功返回 void,失败返回错误码 + * @complexity O(1) + * @thread_safety 单生产者安全 + */ + [[nodiscard]] Result CommitWriteBuffer( + std::uint32_t buffer_index, + const FrameMetadata& metadata, + std::size_t actual_size); + + /** + * @brief 获取读取缓冲区(零拷贝) + * @param timeout 超时时间 + * @return 成功返回帧元数据和数据指针,失败返回错误码 + * @complexity O(1),无内存拷贝 + * @thread_safety 多消费者安全 + * @note 调用者直接读取返回的指针,然后通过 ReleaseReadBuffer 释放 + */ + struct ReadBuffer { + FrameMetadata metadata; ///< 帧元数据 + std::span data; ///< 数据视图(不拥有内存) + std::uint32_t buffer_index{0}; ///< 缓冲区索引(用于释放) + }; + [[nodiscard]] std::expected, ShmTransportError> + AcquireReadBuffer(core::Duration timeout); + + /** + * @brief 释放读取的缓冲区 + * @param buffer_index AcquireReadBuffer 返回的 buffer_index + * @return 成功返回 void,失败返回错误码 + * @complexity O(1) + * @thread_safety 多消费者安全 + */ + [[nodiscard]] Result ReleaseReadBuffer(std::uint32_t buffer_index); + + /** + * @brief 尝试获取写入缓冲区(非阻塞) + * @return 成功返回缓冲区,无可用缓冲区返回 nullopt + */ + [[nodiscard]] std::expected, ShmTransportError> + TryAcquireWriteBuffer(); + + /** + * @brief 尝试获取读取缓冲区(非阻塞) + * @return 成功返回缓冲区,无数据返回 nullopt + */ + [[nodiscard]] std::expected, ShmTransportError> + TryAcquireReadBuffer(); + + // ================================================================== + // 状态查询 + // ================================================================== + + /** + * @brief 获取当前统计信息 + * @return 统计信息副本 + */ + [[nodiscard]] ShmTransportStats GetStats() const noexcept; + + /** + * @brief 获取缓冲区状态 + * @return (已用缓冲区数, 总缓冲区数) + */ + [[nodiscard]] std::pair GetBufferStatus() const noexcept; + + /** + * @brief 获取配置 + * @return 配置副本 + */ + [[nodiscard]] const ShmTransportConfig& GetConfig() const noexcept { return config_; } + + /** + * @brief 获取控制块指针(调试用途) + * @return 控制块指针,未初始化返回 nullptr + */ + [[nodiscard]] const ShmControlBlock* GetControlBlock() const noexcept { return control_block_; } + +private: + // ================================================================== + // 内部方法 + // ================================================================== + + /** + * @brief 更新统计信息 + * @param is_write 是否为写入操作 + * @param bytes 字节数 + * @param latency 操作延迟 + */ + void update_stats(bool is_write, std::size_t bytes, core::Duration latency) noexcept; + + /** + * @brief 获取帧头部地址 + * @param index 缓冲区索引 + * @return 帧头部指针 + */ + [[nodiscard]] ShmFrameHeader* get_frame_header(std::uint32_t index) noexcept; + + /** + * @brief 获取帧数据缓冲区地址 + * @param index 缓冲区索引 + * @return 帧数据缓冲区起始地址 + */ + [[nodiscard]] std::byte* get_frame_buffer(std::uint32_t index) noexcept; + + // ================================================================== + // 成员变量 + // ================================================================== + + ShmTransportConfig config_; ///< 传输配置 + std::unique_ptr segment_; ///< 共享内存段 + ShmControlBlock* control_block_{nullptr}; ///< 控制块指针 + std::byte* frame_data_base_{nullptr}; ///< 帧数据基地址 + + // 状态 + std::atomic initialized_{false}; ///< 初始化标志 + bool is_producer_{false}; ///< 是否为生产者 + + // 统计 + mutable std::mutex stats_mutex_; ///< 统计信息互斥锁 + ShmTransportStats stats_; ///< 本地统计信息 +}; + +// ========================================================================== +// 辅助函数 +// ========================================================================== + +/** + * @brief 生成平台特定的共享内存名称 + * @param name 基础名称 + * @return 完整共享内存名称 + */ +[[nodiscard]] std::string MakeShmName(std::string_view name); + +/** + * @brief 检查共享内存是否存在 + * @param name 共享内存名称 + * @return true 如果存在 + */ +[[nodiscard]] bool ShmExists(std::string_view name); + +/** + * @brief 删除共享内存 + * @param name 共享内存名称 + * @return true 如果成功删除或不存在 + */ +[[nodiscard]] bool ShmRemove(std::string_view name); + +} // namespace aam::l0 + +// 恢复警告设置 +#ifdef _WIN32 +# pragma warning(pop) +#endif + +#endif // AAM_L0_SHM_TRANSPORT_HPP diff --git a/core/src/l0_sensing/shm_transport.cpp b/core/src/l0_sensing/shm_transport.cpp new file mode 100644 index 0000000..3c86577 --- /dev/null +++ b/core/src/l0_sensing/shm_transport.cpp @@ -0,0 +1,1291 @@ +// ========================================================================== +// Copyright (C) 2026 Ethernos Studio +// This file is part of Arknights Auto Machine (AAM). +// +// AAM is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AAM is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with AAM. If not, see . +// ========================================================================== +// @file shm_transport.cpp +// @author dhjs0000 +// @brief L0 共享内存传输层实现 +// ========================================================================== + +#include "aam/l0/shm_transport.hpp" + +#include +#include +#include +#include +#include +#include + +// 平台特定头文件 +#ifdef _WIN32 +# include +#else +# include +# include +# include +# include +#endif + +namespace aam::l0 +{ + +// 使用 core 命名空间 +using namespace aam::core; + +// ========================================================================== +// CRC32 查找表(IEEE 802.3 标准) +// ========================================================================== +namespace +{ +// CRC32 查找表 - 使用 constexpr 确保编译期计算 +constexpr std::array generate_crc32_table() noexcept +{ + std::array table{}; + for (std::uint32_t i = 0; i < 256; ++i) { + std::uint32_t crc = i; + for (std::uint32_t j = 0; j < 8; ++j) { + crc = (crc >> 1) ^ (0xEDB88320 & static_cast(-(static_cast(crc) & 1))); + } + table[i] = crc; + } + return table; +} + +static constexpr auto g_crc32_table = generate_crc32_table(); + +// 错误类别实现 +class ShmTransportErrorCategory : public std::error_category +{ +public: + [[nodiscard]] const char* name() const noexcept override + { + return "shm_transport"; + } + + [[nodiscard]] std::string message(int ev) const override + { + switch (static_cast(ev)) { + case ShmTransportError::Success: + return "Success"; + case ShmTransportError::Unknown: + return "Unknown error"; + case ShmTransportError::InvalidArgument: + return "Invalid argument"; + case ShmTransportError::OutOfMemory: + return "Out of memory"; + case ShmTransportError::Timeout: + return "Operation timeout"; + case ShmTransportError::NotInitialized: + return "Not initialized"; + case ShmTransportError::AlreadyExists: + return "Resource already exists"; + case ShmTransportError::NotFound: + return "Resource not found"; + case ShmTransportError::PermissionDenied: + return "Permission denied"; + case ShmTransportError::ShmCreateFailed: + return "Shared memory creation failed"; + case ShmTransportError::ShmOpenFailed: + return "Shared memory open failed"; + case ShmTransportError::ShmMapFailed: + return "Shared memory mapping failed"; + case ShmTransportError::ShmUnmapFailed: + return "Shared memory unmapping failed"; + case ShmTransportError::ShmTooSmall: + return "Shared memory too small"; + case ShmTransportError::ShmCorrupted: + return "Shared memory data corrupted"; + case ShmTransportError::ShmVersionMismatch: + return "Shared memory version mismatch"; + case ShmTransportError::SyncCreateFailed: + return "Synchronization object creation failed"; + case ShmTransportError::SyncWaitFailed: + return "Synchronization wait failed"; + case ShmTransportError::SyncSignalFailed: + return "Synchronization signal failed"; + case ShmTransportError::SyncTimeout: + return "Synchronization timeout"; + case ShmTransportError::SyncAbandoned: + return "Synchronization object abandoned"; + case ShmTransportError::TransportClosed: + return "Transport closed"; + case ShmTransportError::TransportBusy: + return "Transport busy"; + case ShmTransportError::BufferOverflow: + return "Buffer overflow"; + case ShmTransportError::BufferUnderflow: + return "Buffer underflow"; + case ShmTransportError::FrameTooLarge: + return "Frame too large"; + case ShmTransportError::InvalidFrame: + return "Invalid frame data"; + default: + return "Unknown shared memory transport error"; + } + } + + [[nodiscard]] std::error_condition default_error_condition(int ev) const noexcept override + { + switch (static_cast(ev)) { + case ShmTransportError::Success: + return std::errc{}; + case ShmTransportError::InvalidArgument: + return std::errc::invalid_argument; + case ShmTransportError::OutOfMemory: + return std::errc::not_enough_memory; + case ShmTransportError::Timeout: + case ShmTransportError::SyncTimeout: + return std::errc::timed_out; + case ShmTransportError::NotFound: + return std::errc::no_such_file_or_directory; + case ShmTransportError::AlreadyExists: + return std::errc::file_exists; + case ShmTransportError::PermissionDenied: + return std::errc::permission_denied; + default: + return std::error_condition(ev, *this); + } + } +}; + +// 全局错误类别实例 +const ShmTransportErrorCategory g_shm_transport_error_category{}; + +// 平台特定的共享内存名称前缀 +#ifdef _WIN32 +constexpr std::string_view SHM_NAME_PREFIX = "Local\\"; +#else +constexpr std::string_view SHM_NAME_PREFIX = "/"; +#endif + +} // anonymous namespace + +// ========================================================================== +// 错误类别接口实现 +// ========================================================================== + +const std::error_category& shm_transport_error_category() noexcept +{ + return g_shm_transport_error_category; +} + +// ========================================================================== +// ShmFrameHeader 实现 +// ========================================================================== + +std::uint32_t ShmFrameHeader::calculate_checksum(const std::byte* data) const noexcept +{ + if (data == nullptr || data_size == 0) { + return 0; + } + + std::uint32_t crc = 0xFFFFFFFF; + for (std::uint32_t i = 0; i < data_size; ++i) { + const std::uint8_t byte = static_cast(data[i]); + crc = g_crc32_table[(crc ^ byte) & 0xFF] ^ (crc >> 8); + } + return crc ^ 0xFFFFFFFF; +} + +bool ShmFrameHeader::verify_data(const std::byte* data) const noexcept +{ + if (data == nullptr || data_size == 0) { + return false; + } + return calculate_checksum(data) == checksum; +} + +// ========================================================================== +// Windows 共享内存段实现 +// ========================================================================== +#ifdef _WIN32 + +class WindowsSharedMemorySegment : public SharedMemorySegment +{ +public: + WindowsSharedMemorySegment() noexcept = default; + + ~WindowsSharedMemorySegment() override + { + cleanup(); + } + + WindowsSharedMemorySegment(const WindowsSharedMemorySegment&) = delete; + WindowsSharedMemorySegment& operator=(const WindowsSharedMemorySegment&) = delete; + + WindowsSharedMemorySegment(WindowsSharedMemorySegment&& other) noexcept + : name_(std::move(other.name_)), + size_(other.size_), + handle_(other.handle_), + data_(other.data_) + { + other.handle_ = nullptr; + other.data_ = nullptr; + other.size_ = 0; + } + + WindowsSharedMemorySegment& operator=(WindowsSharedMemorySegment&& other) noexcept + { + if (this != &other) { + cleanup(); + name_ = std::move(other.name_); + size_ = other.size_; + handle_ = other.handle_; + data_ = other.data_; + other.handle_ = nullptr; + other.data_ = nullptr; + other.size_ = 0; + } + return *this; + } + + [[nodiscard]] static std::expected, ShmTransportError> + Create(std::string_view name, std::size_t size) + { + if (name.empty() || size == 0) { + return std::unexpected(ShmTransportError::InvalidArgument); + } + + auto segment = std::make_unique(); + segment->name_ = std::string(name); + segment->size_ = size; + + segment->handle_ = CreateFileMappingA( + INVALID_HANDLE_VALUE, + nullptr, + PAGE_READWRITE, + static_cast((size >> 32) & 0xFFFFFFFF), + static_cast(size & 0xFFFFFFFF), + segment->name_.c_str() + ); + + if (segment->handle_ == nullptr) { + return std::unexpected(ShmTransportError::ShmCreateFailed); + } + + // 检查是否创建了新的共享内存还是打开了已存在的 + // CreateFileMappingA 在打开已存在的映射时也会返回有效句柄 + const DWORD create_error = GetLastError(); + const bool already_exists = (create_error == ERROR_ALREADY_EXISTS); + + segment->data_ = static_cast(MapViewOfFile( + segment->handle_, + FILE_MAP_ALL_ACCESS, + 0, + 0, + size + )); + + if (segment->data_ == nullptr) { + // 映射失败时关闭句柄避免泄漏 + CloseHandle(segment->handle_); + segment->handle_ = nullptr; + return std::unexpected(ShmTransportError::ShmMapFailed); + } + + // 如果是已存在的映射,在映射成功后返回错误 + // 注意:必须先关闭句柄和解除映射 + if (already_exists) { + UnmapViewOfFile(segment->data_); + CloseHandle(segment->handle_); + segment->data_ = nullptr; + segment->handle_ = nullptr; + return std::unexpected(ShmTransportError::AlreadyExists); + } + + std::memset(segment->data_, 0, size); + return segment; + } + + [[nodiscard]] static std::expected, ShmTransportError> + Open(std::string_view name, std::size_t size) + { + if (name.empty() || size == 0) { + return std::unexpected(ShmTransportError::InvalidArgument); + } + + auto segment = std::make_unique(); + segment->name_ = std::string(name); + segment->size_ = size; + + segment->handle_ = OpenFileMappingA( + FILE_MAP_ALL_ACCESS, + FALSE, + segment->name_.c_str() + ); + + if (segment->handle_ == nullptr) { + const DWORD error = GetLastError(); + if (error == ERROR_FILE_NOT_FOUND) { + return std::unexpected(ShmTransportError::NotFound); + } + return std::unexpected(ShmTransportError::ShmOpenFailed); + } + + segment->data_ = static_cast(MapViewOfFile( + segment->handle_, + FILE_MAP_ALL_ACCESS, + 0, + 0, + size + )); + + if (segment->data_ == nullptr) { + return std::unexpected(ShmTransportError::ShmMapFailed); + } + + return segment; + } + + [[nodiscard]] std::byte* data() const noexcept override { return data_; } + [[nodiscard]] std::size_t size() const noexcept override { return size_; } + [[nodiscard]] std::string_view name() const noexcept override { return name_; } + [[nodiscard]] bool is_valid() const noexcept override { return handle_ != nullptr && data_ != nullptr; } + + [[nodiscard]] Result flush() noexcept override + { + if (data_ == nullptr) { + return std::unexpected(ShmTransportError::NotInitialized); + } + if (!FlushViewOfFile(data_, size_)) { + return std::unexpected(ShmTransportError::Unknown); + } + return {}; + } + +private: + void cleanup() noexcept + { + if (data_ != nullptr) { + UnmapViewOfFile(data_); + data_ = nullptr; + } + if (handle_ != nullptr) { + CloseHandle(handle_); + handle_ = nullptr; + } + } + + std::string name_; + std::size_t size_{0}; + HANDLE handle_{nullptr}; + std::byte* data_{nullptr}; +}; + +using PlatformSharedMemorySegment = WindowsSharedMemorySegment; + +// ========================================================================== +// POSIX 共享内存段实现 +// ========================================================================== +#else + +class PosixSharedMemorySegment : public SharedMemorySegment +{ +public: + PosixSharedMemorySegment() noexcept = default; + + ~PosixSharedMemorySegment() override + { + cleanup(); + } + + PosixSharedMemorySegment(const PosixSharedMemorySegment&) = delete; + PosixSharedMemorySegment& operator=(const PosixSharedMemorySegment&) = delete; + + PosixSharedMemorySegment(PosixSharedMemorySegment&& other) noexcept + : name_(std::move(other.name_)), + size_(other.size_), + fd_(other.fd_), + data_(other.data_), + created_(other.created_) + { + other.fd_ = -1; + other.data_ = nullptr; + other.size_ = 0; + other.created_ = false; + } + + PosixSharedMemorySegment& operator=(PosixSharedMemorySegment&& other) noexcept + { + if (this != &other) { + cleanup(); + name_ = std::move(other.name_); + size_ = other.size_; + fd_ = other.fd_; + data_ = other.data_; + created_ = other.created_; + other.fd_ = -1; + other.data_ = nullptr; + other.size_ = 0; + other.created_ = false; + } + return *this; + } + + [[nodiscard]] static std::expected, ShmTransportError> + Create(std::string_view name, std::size_t size) + { + if (name.empty() || size == 0) { + return std::unexpected(ShmTransportError::InvalidArgument); + } + + auto segment = std::make_unique(); + segment->name_ = std::string(name); + segment->size_ = size; + segment->created_ = true; + + segment->fd_ = shm_open(segment->name_.c_str(), O_CREAT | O_EXCL | O_RDWR, 0666); + if (segment->fd_ == -1) { + if (errno == EEXIST) { + return std::unexpected(ShmTransportError::AlreadyExists); + } + return std::unexpected(ShmTransportError::ShmCreateFailed); + } + + if (ftruncate(segment->fd_, static_cast(size)) == -1) { + shm_unlink(segment->name_.c_str()); + return std::unexpected(ShmTransportError::ShmCreateFailed); + } + + segment->data_ = static_cast(mmap( + nullptr, + size, + PROT_READ | PROT_WRITE, + MAP_SHARED, + segment->fd_, + 0 + )); + + if (segment->data_ == MAP_FAILED) { + shm_unlink(segment->name_.c_str()); + segment->data_ = nullptr; + return std::unexpected(ShmTransportError::ShmMapFailed); + } + + std::memset(segment->data_, 0, size); + return segment; + } + + [[nodiscard]] static std::expected, ShmTransportError> + Open(std::string_view name, std::size_t size) + { + if (name.empty() || size == 0) { + return std::unexpected(ShmTransportError::InvalidArgument); + } + + auto segment = std::make_unique(); + segment->name_ = std::string(name); + segment->size_ = size; + segment->created_ = false; + + segment->fd_ = shm_open(segment->name_.c_str(), O_RDWR, 0666); + if (segment->fd_ == -1) { + if (errno == ENOENT) { + return std::unexpected(ShmTransportError::NotFound); + } + return std::unexpected(ShmTransportError::ShmOpenFailed); + } + + segment->data_ = static_cast(mmap( + nullptr, + size, + PROT_READ | PROT_WRITE, + MAP_SHARED, + segment->fd_, + 0 + )); + + if (segment->data_ == MAP_FAILED) { + segment->data_ = nullptr; + return std::unexpected(ShmTransportError::ShmMapFailed); + } + + return segment; + } + + [[nodiscard]] std::byte* data() const noexcept override { return data_; } + [[nodiscard]] std::size_t size() const noexcept override { return size_; } + [[nodiscard]] std::string_view name() const noexcept override { return name_; } + [[nodiscard]] bool is_valid() const noexcept override { return fd_ != -1 && data_ != nullptr; } + + [[nodiscard]] Result flush() noexcept override + { + if (data_ == nullptr) { + return std::unexpected(ShmTransportError::NotInitialized); + } + if (msync(data_, size_, MS_SYNC) == -1) { + return std::unexpected(ShmTransportError::Unknown); + } + return {}; + } + +private: + void cleanup() noexcept + { + if (data_ != nullptr) { + munmap(data_, size_); + data_ = nullptr; + } + if (fd_ != -1) { + close(fd_); + fd_ = -1; + } + if (created_ && !name_.empty()) { + shm_unlink(name_.c_str()); + } + } + + std::string name_; + std::size_t size_{0}; + int fd_{-1}; + std::byte* data_{nullptr}; + bool created_{false}; +}; + +using PlatformSharedMemorySegment = PosixSharedMemorySegment; + +#endif // _WIN32 + +// ========================================================================== +// SharedMemorySegment 工厂方法实现 +// ========================================================================== + +std::expected, ShmTransportError> +SharedMemorySegment::Create(std::string_view name, std::size_t size) +{ + return PlatformSharedMemorySegment::Create(name, size); +} + +std::expected, ShmTransportError> +SharedMemorySegment::Open(std::string_view name, std::size_t size) +{ + return PlatformSharedMemorySegment::Open(name, size); +} + +// ========================================================================== +// ShmTransport 实现 +// ========================================================================== + +ShmTransport::~ShmTransport() noexcept +{ + [[maybe_unused]] auto result = Shutdown(); +} + +ShmTransport::ShmTransport(ShmTransport&& other) noexcept + : config_(std::move(other.config_)), + segment_(std::move(other.segment_)), + control_block_(other.control_block_), + frame_data_base_(other.frame_data_base_), + initialized_(other.initialized_.load()), + is_producer_(other.is_producer_), + stats_(std::move(other.stats_)) +{ + other.control_block_ = nullptr; + other.frame_data_base_ = nullptr; + other.initialized_ = false; + other.is_producer_ = false; +} + +ShmTransport& ShmTransport::operator=(ShmTransport&& other) noexcept +{ + if (this != &other) { + [[maybe_unused]] auto _ = Shutdown(); + config_ = std::move(other.config_); + segment_ = std::move(other.segment_); + control_block_ = other.control_block_; + frame_data_base_ = other.frame_data_base_; + initialized_ = other.initialized_.load(); + is_producer_ = other.is_producer_; + stats_ = std::move(other.stats_); + other.control_block_ = nullptr; + other.frame_data_base_ = nullptr; + other.initialized_ = false; + other.is_producer_ = false; + } + return *this; +} + +ShmTransport::Result ShmTransport::InitializeProducer(const ShmTransportConfig& config) +{ + if (initialized_.load(std::memory_order_acquire)) { + return std::unexpected(ShmTransportError::AlreadyExists); + } + + auto validation = config.validate(); + if (!validation) { + return validation; + } + + const std::string full_name = MakeShmName(config.shm_name); + const std::size_t total_size = config.calculate_total_size(); + + auto segment_result = SharedMemorySegment::Create(full_name, total_size); + if (!segment_result) { + return std::unexpected(segment_result.error()); + } + + segment_ = std::move(*segment_result); + config_ = config; + is_producer_ = true; + + control_block_ = reinterpret_cast(segment_->data()); + frame_data_base_ = segment_->data() + sizeof(ShmControlBlock) + config.metadata_size; + + const std::size_t frame_header_size = sizeof(ShmFrameHeader); + const std::size_t aligned_frame_size = + ((frame_header_size + config.max_frame_size + 63) / 64) * 64; + + control_block_->magic = ShmControlBlock::MAGIC; + control_block_->version = ShmControlBlock::VERSION; + control_block_->header_size = static_cast(sizeof(ShmControlBlock) + config.metadata_size); + control_block_->flags = 0; + control_block_->buffer_count = static_cast(config.buffer_count); + control_block_->max_frame_size = static_cast(config.max_frame_size); + control_block_->metadata_size = static_cast(config.metadata_size); + control_block_->frame_stride = static_cast(aligned_frame_size & 0xFFFFFFFFULL); + + control_block_->write_sequence.store(0, std::memory_order_relaxed); + control_block_->read_sequence.store(0, std::memory_order_relaxed); + control_block_->dropped_frames.store(0, std::memory_order_relaxed); + control_block_->state.store(ShmControlBlock::STATE_INITIALIZED, std::memory_order_relaxed); + control_block_->active_readers.store(0, std::memory_order_relaxed); + control_block_->active_writers.store(1, std::memory_order_relaxed); + control_block_->total_frames_written.store(0, std::memory_order_relaxed); + control_block_->total_frames_read.store(0, std::memory_order_relaxed); + control_block_->total_bytes_written.store(0, std::memory_order_relaxed); + control_block_->total_bytes_read.store(0, std::memory_order_relaxed); + + stats_.reset(); + initialized_.store(true, std::memory_order_release); + + return {}; +} + +ShmTransport::Result ShmTransport::InitializeConsumer(const ShmTransportConfig& config) +{ + if (initialized_.load(std::memory_order_acquire)) { + return std::unexpected(ShmTransportError::AlreadyExists); + } + + auto validation = config.validate(); + if (!validation) { + return validation; + } + + const std::string full_name = MakeShmName(config.shm_name); + const std::size_t total_size = config.calculate_total_size(); + + auto segment_result = SharedMemorySegment::Open(full_name, total_size); + if (!segment_result) { + return std::unexpected(segment_result.error()); + } + + segment_ = std::move(*segment_result); + config_ = config; + is_producer_ = false; + + control_block_ = reinterpret_cast(segment_->data()); + frame_data_base_ = segment_->data() + sizeof(ShmControlBlock) + config.metadata_size; + + if (!control_block_->is_valid()) { + segment_.reset(); + control_block_ = nullptr; + frame_data_base_ = nullptr; + return std::unexpected(ShmTransportError::ShmCorrupted); + } + + control_block_->active_readers.fetch_add(1, std::memory_order_relaxed); + + stats_.reset(); + initialized_.store(true, std::memory_order_release); + + return {}; +} + +ShmTransport::Result ShmTransport::Shutdown() noexcept +{ + if (!initialized_.load(std::memory_order_acquire)) { + return {}; + } + + if (control_block_ != nullptr && !is_producer_) { + control_block_->active_readers.fetch_sub(1, std::memory_order_relaxed); + } + + segment_.reset(); + control_block_ = nullptr; + frame_data_base_ = nullptr; + initialized_.store(false, std::memory_order_release); + is_producer_ = false; + + return {}; +} + +bool ShmTransport::IsInitialized() const noexcept +{ + return initialized_.load(std::memory_order_acquire); +} + +bool ShmTransport::IsProducer() const noexcept +{ + return initialized_.load(std::memory_order_acquire) && is_producer_; +} + +bool ShmTransport::IsConsumer() const noexcept +{ + return initialized_.load(std::memory_order_acquire) && !is_producer_; +} + +ShmTransport::Result ShmTransport::WriteFrame(const FrameMetadata& metadata, + std::span data) +{ + // 前置校验:检查空数据 + if (data.empty()) { + return std::unexpected(ShmTransportError::InvalidFrame); + } + + // 如果启用零拷贝模式,使用零拷贝 API + if (config_.enable_zero_copy) { + auto buffer_result = AcquireWriteBuffer(config_.write_timeout); + if (!buffer_result) { + return std::unexpected(buffer_result.error()); + } + if (!buffer_result->has_value()) { + return std::unexpected(ShmTransportError::Timeout); + } + + auto& buffer = buffer_result->value(); + if (data.size() > buffer.capacity) { + return std::unexpected(ShmTransportError::FrameTooLarge); + } + + // 直接复制到共享内存缓冲区 + std::memcpy(buffer.data, data.data(), data.size()); + + return CommitWriteBuffer(buffer.buffer_index, metadata, data.size()); + } + + // 非零拷贝模式:使用传统路径 + auto result = WriteFrameWithTimeout(metadata, data, config_.write_timeout); + if (!result) { + return std::unexpected(result.error()); + } + return {}; +} + +std::expected ShmTransport::TryWriteFrame( + const FrameMetadata& metadata, std::span data) +{ + if (!initialized_.load(std::memory_order_acquire) || !is_producer_) { + return std::unexpected(ShmTransportError::NotInitialized); + } + + if (!metadata.is_valid()) { + return std::unexpected(ShmTransportError::InvalidFrame); + } + + if (data.size() > config_.max_frame_size) { + return std::unexpected(ShmTransportError::FrameTooLarge); + } + + if (!control_block_->is_writable()) { + return false; + } + + const auto start_time = core::Clock::now(); + const std::uint32_t write_index = control_block_->get_write_index(); + + ShmFrameHeader* header = get_frame_header(write_index); + std::byte* frame_data = get_frame_buffer(write_index) + sizeof(ShmFrameHeader); + + header->magic = ShmFrameHeader::MAGIC; + header->version = 1; + header->sequence_number = static_cast( + control_block_->write_sequence.load(std::memory_order_relaxed)); + header->frame_number = static_cast(metadata.frame_number); + header->sequence_id = static_cast(metadata.sequence_id); + header->data_size = static_cast(data.size()); + header->capture_timestamp_ns = static_cast( + metadata.capture_timestamp.time_since_epoch().count()); + header->write_timestamp_ns = static_cast( + core::Clock::now().time_since_epoch().count()); + header->width = metadata.width; + header->height = metadata.height; + header->stride = metadata.stride; + header->pixel_format = static_cast(metadata.pixel_format); + + std::memcpy(frame_data, data.data(), data.size()); + + // 仅在启用校验和时计算CRC + if (config_.enable_checksum) { + header->checksum = header->calculate_checksum(frame_data); + } else { + header->checksum = 0; + } + + control_block_->write_sequence.fetch_add(1, std::memory_order_release); + control_block_->total_frames_written.fetch_add(1, std::memory_order_relaxed); + control_block_->total_bytes_written.fetch_add(data.size(), std::memory_order_relaxed); + + const auto latency = core::Clock::now() - start_time; + update_stats(true, data.size(), latency); + + return true; +} + +std::expected ShmTransport::WriteFrameWithTimeout( + const FrameMetadata& metadata, std::span data, core::Duration timeout) +{ + if (!initialized_.load(std::memory_order_acquire) || !is_producer_) { + return std::unexpected(ShmTransportError::NotInitialized); + } + + const auto deadline = core::Clock::now() + timeout; + + while (core::Clock::now() < deadline) { + auto result = TryWriteFrame(metadata, data); + if (!result) { + return result; + } + if (*result) { + return true; + } + std::this_thread::yield(); + } + + control_block_->dropped_frames.fetch_add(1, std::memory_order_relaxed); + stats_.write_timeouts++; + return false; +} + +std::expected>>, ShmTransportError> +ShmTransport::ReadFrame(core::Duration timeout) +{ + if (!initialized_.load(std::memory_order_acquire) || is_producer_) { + return std::unexpected(ShmTransportError::NotInitialized); + } + + // 如果启用零拷贝模式,使用零拷贝 API 读取 + if (config_.enable_zero_copy) { + auto buffer_result = AcquireReadBuffer(timeout); + if (!buffer_result) { + return std::unexpected(buffer_result.error()); + } + if (!buffer_result->has_value()) { + stats_.read_timeouts++; + return std::optional>>{}; + } + + const auto& buffer = buffer_result->value(); + + // 复制数据到本地缓冲区 + std::vector data(buffer.data.size()); + std::memcpy(data.data(), buffer.data.data(), buffer.data.size()); + + FrameMetadata metadata = buffer.metadata; + + // 释放读取缓冲区 + auto release_result = ReleaseReadBuffer(buffer.buffer_index); + if (!release_result.has_value()) { + return std::unexpected(release_result.error()); + } + + return std::make_optional(std::make_pair(std::move(metadata), std::move(data))); + } + + // 非零拷贝模式:使用传统路径 + const auto deadline = core::Clock::now() + timeout; + + while (core::Clock::now() < deadline) { + auto result = TryReadFrame(); + if (!result) { + return result; + } + if (result->has_value()) { + return result; + } + std::this_thread::yield(); + } + + stats_.read_timeouts++; + return std::optional>>{}; +} + +std::expected>>, ShmTransportError> +ShmTransport::TryReadFrame() +{ + if (!initialized_.load(std::memory_order_acquire) || is_producer_) { + return std::unexpected(ShmTransportError::NotInitialized); + } + + if (!control_block_->has_readable_data()) { + return std::optional>>{}; + } + + const auto start_time = core::Clock::now(); + const std::uint32_t read_index = control_block_->get_read_index(); + + ShmFrameHeader* header = get_frame_header(read_index); + std::byte* frame_data = get_frame_buffer(read_index) + sizeof(ShmFrameHeader); + + if (!header->is_valid()) { + return std::unexpected(ShmTransportError::ShmCorrupted); + } + + // 仅在启用校验和时验证数据完整性 + if (config_.enable_checksum && !header->verify_data(frame_data)) { + stats_.checksum_errors++; + control_block_->read_sequence.fetch_add(1, std::memory_order_release); + return std::unexpected(ShmTransportError::ShmCorrupted); + } + + FrameMetadata metadata; + metadata.width = header->width; + metadata.height = header->height; + metadata.stride = header->stride; + metadata.pixel_format = static_cast(header->pixel_format); + metadata.capture_timestamp = core::Timestamp(std::chrono::nanoseconds(header->capture_timestamp_ns)); + metadata.process_timestamp = core::Clock::now(); + metadata.frame_number = header->frame_number; + metadata.sequence_id = header->sequence_id; + metadata.data_size = header->data_size; + + std::vector data(header->data_size); + std::memcpy(data.data(), frame_data, header->data_size); + + control_block_->read_sequence.fetch_add(1, std::memory_order_release); + control_block_->total_frames_read.fetch_add(1, std::memory_order_relaxed); + control_block_->total_bytes_read.fetch_add(header->data_size, std::memory_order_relaxed); + + const auto latency = core::Clock::now() - start_time; + update_stats(false, header->data_size, latency); + + return std::make_optional(std::make_pair(std::move(metadata), std::move(data))); +} + +ShmTransport::Result ShmTransport::ReadFrameWithCallback(core::Duration timeout, ReadCallback callback) +{ + if (!callback) { + return std::unexpected(ShmTransportError::InvalidArgument); + } + + auto result = ReadFrame(timeout); + if (!result) { + return std::unexpected(result.error()); + } + + if (result->has_value()) { + const auto& [metadata, data] = result->value(); + callback(metadata, std::span(data.data(), data.size())); + } + + return {}; +} + +ShmTransportStats ShmTransport::GetStats() const noexcept +{ + std::lock_guard lock(stats_mutex_); + ShmTransportStats result = stats_; // 使用显式拷贝构造函数 + + if (control_block_ != nullptr) { + result.frames_dropped = control_block_->dropped_frames.load(std::memory_order_relaxed); + } + return result; +} + +std::pair ShmTransport::GetBufferStatus() const noexcept +{ + if (!initialized_.load(std::memory_order_acquire) || control_block_ == nullptr) { + return {0, 0}; + } + return {control_block_->used_buffers(), control_block_->buffer_count}; +} + +void ShmTransport::update_stats(bool is_write, std::size_t bytes, core::Duration latency) noexcept +{ + std::lock_guard lock(stats_mutex_); + if (is_write) { + stats_.frames_written++; + stats_.bytes_written += bytes; + stats_.update_write_latency(latency); + stats_.last_write_time = core::Clock::now(); + } else { + stats_.frames_read++; + stats_.bytes_read += bytes; + stats_.update_read_latency(latency); + stats_.last_read_time = core::Clock::now(); + } +} + +ShmFrameHeader* ShmTransport::get_frame_header(std::uint32_t index) noexcept +{ + return reinterpret_cast(get_frame_buffer(index)); +} + +std::byte* ShmTransport::get_frame_buffer(std::uint32_t index) noexcept +{ + if (control_block_ == nullptr || segment_ == nullptr) { + return nullptr; + } + const std::size_t header_size = sizeof(ShmControlBlock) + config_.metadata_size; + const std::size_t offset = header_size + (index * control_block_->frame_stride); + return segment_->data() + offset; +} + +// ========================================================================== +// 辅助函数实现 +// ========================================================================== + +std::string MakeShmName(std::string_view name) +{ +#ifdef _WIN32 + return std::string("Local\\") + std::string(name); +#else + return std::string("/") + std::string(name); +#endif +} + +bool ShmExists(std::string_view name) +{ +#ifdef _WIN32 + const std::string full_name = MakeShmName(name); + HANDLE handle = OpenFileMappingA(FILE_MAP_READ, FALSE, full_name.c_str()); + if (handle != nullptr) { + CloseHandle(handle); + return true; + } + return false; +#else + const std::string full_name = MakeShmName(name); + int fd = shm_open(full_name.c_str(), O_RDONLY, 0666); + if (fd != -1) { + close(fd); + return true; + } + return false; +#endif +} + +bool ShmRemove([[maybe_unused]] std::string_view name) +{ +#ifdef _WIN32 + // Windows: 共享内存在最后一个句柄关闭后自动删除 + (void)name; + return true; +#else + const std::string full_name = MakeShmName(name); + return shm_unlink(full_name.c_str()) == 0 || errno == ENOENT; +#endif +} + + +// ========================================================================== +// 零拷贝API实现 +// ========================================================================== + +std::expected, ShmTransportError> +ShmTransport::AcquireWriteBuffer(core::Duration timeout) +{ + if (!initialized_.load(std::memory_order_acquire) || !is_producer_) { + return std::unexpected(ShmTransportError::NotInitialized); + } + + const auto deadline = core::Clock::now() + timeout; + + while (core::Clock::now() < deadline) { + auto result = TryAcquireWriteBuffer(); + if (!result) { + return result; + } + if (result->has_value()) { + return result; + } + std::this_thread::yield(); + } + + stats_.write_timeouts++; + return std::optional{}; +} + +std::expected, ShmTransportError> +ShmTransport::TryAcquireWriteBuffer() +{ + if (!initialized_.load(std::memory_order_acquire) || !is_producer_) { + return std::unexpected(ShmTransportError::NotInitialized); + } + + if (!control_block_->is_writable()) { + return std::optional{}; + } + + const std::uint32_t write_index = control_block_->get_write_index(); + std::byte* frame_data = get_frame_buffer(write_index) + sizeof(ShmFrameHeader); + + WriteBuffer buffer; + buffer.data = frame_data; + buffer.capacity = config_.max_frame_size; + buffer.buffer_index = write_index; + + return std::make_optional(buffer); +} + +ShmTransport::Result ShmTransport::CommitWriteBuffer( + std::uint32_t buffer_index, + const FrameMetadata& metadata, + std::size_t actual_size) +{ + if (!initialized_.load(std::memory_order_acquire) || !is_producer_) { + return std::unexpected(ShmTransportError::NotInitialized); + } + + if (actual_size > config_.max_frame_size) { + return std::unexpected(ShmTransportError::FrameTooLarge); + } + + const auto start_time = core::Clock::now(); + ShmFrameHeader* header = get_frame_header(buffer_index); + std::byte* frame_data = get_frame_buffer(buffer_index) + sizeof(ShmFrameHeader); + + // 填充帧头部(零拷贝模式下跳过CRC计算) + header->magic = ShmFrameHeader::MAGIC; + header->version = 1; + header->sequence_number = static_cast( + control_block_->write_sequence.load(std::memory_order_relaxed)); + header->frame_number = static_cast(metadata.frame_number); + header->sequence_id = static_cast(metadata.sequence_id); + header->data_size = static_cast(actual_size); + header->capture_timestamp_ns = static_cast( + metadata.capture_timestamp.time_since_epoch().count()); + header->write_timestamp_ns = static_cast( + core::Clock::now().time_since_epoch().count()); + header->width = metadata.width; + header->height = metadata.height; + header->stride = metadata.stride; + header->pixel_format = static_cast(metadata.pixel_format); + + // 仅在启用校验和时计算CRC + if (config_.enable_checksum) { + header->checksum = header->calculate_checksum(frame_data); + } else { + header->checksum = 0; + } + + // 发布内存屏障,确保数据在序列号更新前可见 + std::atomic_thread_fence(std::memory_order_release); + + control_block_->write_sequence.fetch_add(1, std::memory_order_release); + control_block_->total_frames_written.fetch_add(1, std::memory_order_relaxed); + control_block_->total_bytes_written.fetch_add(actual_size, std::memory_order_relaxed); + + const auto latency = core::Clock::now() - start_time; + update_stats(true, actual_size, latency); + + return {}; +} + +std::expected, ShmTransportError> +ShmTransport::AcquireReadBuffer(core::Duration timeout) +{ + if (!initialized_.load(std::memory_order_acquire) || is_producer_) { + return std::unexpected(ShmTransportError::NotInitialized); + } + + const auto deadline = core::Clock::now() + timeout; + + while (core::Clock::now() < deadline) { + auto result = TryAcquireReadBuffer(); + if (!result) { + return result; + } + if (result->has_value()) { + return result; + } + std::this_thread::yield(); + } + + stats_.read_timeouts++; + return std::optional{}; +} + +std::expected, ShmTransportError> +ShmTransport::TryAcquireReadBuffer() +{ + if (!initialized_.load(std::memory_order_acquire) || is_producer_) { + return std::unexpected(ShmTransportError::NotInitialized); + } + + if (!control_block_->has_readable_data()) { + return std::optional{}; + } + + const std::uint32_t read_index = control_block_->get_read_index(); + ShmFrameHeader* header = get_frame_header(read_index); + std::byte* frame_data = get_frame_buffer(read_index) + sizeof(ShmFrameHeader); + + // 获取内存屏障,确保看到最新的数据 + std::atomic_thread_fence(std::memory_order_acquire); + + if (!header->is_valid()) { + return std::unexpected(ShmTransportError::ShmCorrupted); + } + + // 仅在启用校验和时验证 + if (config_.enable_checksum && !header->verify_data(frame_data)) { + stats_.checksum_errors++; + // 跳过损坏的帧 + control_block_->read_sequence.fetch_add(1, std::memory_order_release); + return std::unexpected(ShmTransportError::ShmCorrupted); + } + + FrameMetadata metadata; + metadata.width = header->width; + metadata.height = header->height; + metadata.stride = header->stride; + metadata.pixel_format = static_cast(header->pixel_format); + metadata.capture_timestamp = core::Timestamp(std::chrono::nanoseconds(header->capture_timestamp_ns)); + metadata.process_timestamp = core::Clock::now(); + metadata.frame_number = header->frame_number; + metadata.sequence_id = header->sequence_id; + metadata.data_size = header->data_size; + + ReadBuffer buffer; + buffer.metadata = metadata; + buffer.data = std::span(frame_data, header->data_size); + buffer.buffer_index = read_index; + + return std::make_optional(buffer); +} + +ShmTransport::Result ShmTransport::ReleaseReadBuffer(std::uint32_t buffer_index) +{ + if (!initialized_.load(std::memory_order_acquire) || is_producer_) { + return std::unexpected(ShmTransportError::NotInitialized); + } + + [[maybe_unused]] const auto start_time = core::Clock::now(); + ShmFrameHeader* header = get_frame_header(buffer_index); + + control_block_->read_sequence.fetch_add(1, std::memory_order_release); + control_block_->total_frames_read.fetch_add(1, std::memory_order_relaxed); + control_block_->total_bytes_read.fetch_add(header->data_size, std::memory_order_relaxed); + + const auto latency = core::Clock::now() - start_time; + update_stats(false, header->data_size, latency); + + return {}; +} + +} // namespace aam::l0 \ No newline at end of file diff --git a/core/tests/CMakeLists.txt b/core/tests/CMakeLists.txt index cd08c7d..502f969 100644 --- a/core/tests/CMakeLists.txt +++ b/core/tests/CMakeLists.txt @@ -29,6 +29,7 @@ set(CORE_TEST_SOURCES test_memory_pool.cpp test_timer.cpp test_frame_buffer.cpp + test_shm_transport.cpp ) # 创建测试可执行文件 diff --git a/core/tests/test_shm_transport.cpp b/core/tests/test_shm_transport.cpp new file mode 100644 index 0000000..affff8c --- /dev/null +++ b/core/tests/test_shm_transport.cpp @@ -0,0 +1,1431 @@ +// ========================================================================== +// Copyright (C) 2026 Ethernos Studio +// This file is part of Arknights Auto Machine (AAM). +// +// AAM is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AAM is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with AAM. If not, see . +// ========================================================================== +// @file test_shm_transport.cpp +// @author dhjs0000 +// @brief 共享内存传输层单元测试 +// ========================================================================== + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "aam/l0/shm_transport.hpp" + +using namespace aam::l0; +using namespace std::chrono_literals; + +// 使用 Clock 别名避免 core:: 前缀 +using Clock = aam::core::Clock; + +// ========================================================================== +// 测试夹具 +// ========================================================================== + +class ShmTransportTest : public ::testing::Test +{ +protected: + void SetUp() override + { + // 清理可能存在的旧共享内存 + [[maybe_unused]] auto _ = ShmRemove(test_shm_name_); + } + + void TearDown() override + { + // 清理测试创建的共享内存 + [[maybe_unused]] auto _ = ShmRemove(test_shm_name_); + } + + // 生成唯一测试名称 + static std::string GenerateUniqueName() + { + static std::atomic counter{0}; + const auto now = std::chrono::steady_clock::now().time_since_epoch().count(); + return "aam_test_" + std::to_string(now) + "_" + std::to_string(counter.fetch_add(1)); + } + + const std::string test_shm_name_ = GenerateUniqueName(); + static constexpr std::size_t kTestBufferCount = 16; // 增加缓冲区大小以支持多帧测试 + static constexpr std::size_t kTestFrameSize = 1024 * 1024; // 1MB +}; + +// ========================================================================== +// 基础功能测试 +// ========================================================================== + +TEST_F(ShmTransportTest, ConfigValidation) +{ + ShmTransportConfig config; + + // 有效配置 + config.shm_name = "test"; + config.buffer_count = 4; + config.max_frame_size = 1024; + EXPECT_TRUE(config.validate().has_value()); + + // 无效:空名称 + config.shm_name = ""; + EXPECT_FALSE(config.validate().has_value()); + config.shm_name = "test"; + + // 无效:零缓冲区 + config.buffer_count = 0; + EXPECT_FALSE(config.validate().has_value()); + config.buffer_count = 4; + + // 无效:过多缓冲区 + config.buffer_count = 100; + EXPECT_FALSE(config.validate().has_value()); + config.buffer_count = 4; + + // 无效:零帧大小 + config.max_frame_size = 0; + EXPECT_FALSE(config.validate().has_value()); + config.max_frame_size = 1024; + + // 无效:过大帧大小 + config.max_frame_size = 300 * 1024 * 1024; // 300MB + EXPECT_FALSE(config.validate().has_value()); +} + +TEST_F(ShmTransportTest, ConfigSizeCalculation) +{ + ShmTransportConfig config; + config.buffer_count = 4; + config.max_frame_size = 1024; + config.metadata_size = 4096; + + const std::size_t total_size = config.calculate_total_size(); + + // 验证大小大于等于头部 + 缓冲区(考虑对齐后可能相等) + EXPECT_GE(total_size, sizeof(ShmControlBlock) + config.metadata_size); + EXPECT_GE(total_size, sizeof(ShmControlBlock) + config.metadata_size + + 4 * (sizeof(ShmFrameHeader) + 1024)); +} + +TEST_F(ShmTransportTest, ProducerInitialization) +{ + ShmTransport producer; + + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = kTestBufferCount; + config.max_frame_size = kTestFrameSize; + + // 初始化生产者 + auto result = producer.InitializeProducer(config); + EXPECT_TRUE(result.has_value()) << "Producer initialization failed"; + EXPECT_TRUE(producer.IsInitialized()); + EXPECT_TRUE(producer.IsProducer()); + EXPECT_FALSE(producer.IsConsumer()); + + // 获取控制块验证 + const auto* control_block = producer.GetControlBlock(); + ASSERT_NE(control_block, nullptr); + EXPECT_TRUE(control_block->is_valid()); + EXPECT_EQ(control_block->buffer_count, kTestBufferCount); + EXPECT_EQ(control_block->max_frame_size, kTestFrameSize); + + // 关闭 + auto shutdown_result = producer.Shutdown(); + EXPECT_TRUE(shutdown_result.has_value()); + EXPECT_FALSE(producer.IsInitialized()); +} + +TEST_F(ShmTransportTest, ConsumerInitialization) +{ + // 先创建生产者 + ShmTransport producer; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = kTestBufferCount; + config.max_frame_size = kTestFrameSize; + + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + // 再创建消费者 + ShmTransport consumer; + auto result = consumer.InitializeConsumer(config); + EXPECT_TRUE(result.has_value()) << "Consumer initialization failed"; + EXPECT_TRUE(consumer.IsInitialized()); + EXPECT_FALSE(consumer.IsProducer()); + EXPECT_TRUE(consumer.IsConsumer()); + + // 验证控制块 + const auto* control_block = consumer.GetControlBlock(); + ASSERT_NE(control_block, nullptr); + EXPECT_TRUE(control_block->is_valid()); +} + +TEST_F(ShmTransportTest, ConsumerBeforeProducer) +{ + ShmTransport consumer; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = kTestBufferCount; + config.max_frame_size = kTestFrameSize; + + // 消费者先初始化应该失败(共享内存不存在) + auto result = consumer.InitializeConsumer(config); + EXPECT_FALSE(result.has_value()); +} + +TEST_F(ShmTransportTest, DoubleInitialization) +{ + ShmTransport transport; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = kTestBufferCount; + config.max_frame_size = kTestFrameSize; + + ASSERT_TRUE(transport.InitializeProducer(config).has_value()); + + // 重复初始化应该失败 + auto result = transport.InitializeProducer(config); + EXPECT_FALSE(result.has_value()); +} + +// ========================================================================== +// 帧传输测试 +// ========================================================================== + +TEST_F(ShmTransportTest, BasicFrameWriteRead) +{ + // 创建生产者 + ShmTransport producer; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = kTestBufferCount; + config.max_frame_size = kTestFrameSize; + config.write_timeout = 100ms; + config.read_timeout = 100ms; + + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + // 创建消费者 + ShmTransport consumer; + ASSERT_TRUE(consumer.InitializeConsumer(config).has_value()); + + // 准备测试数据 + FrameMetadata write_metadata; + write_metadata.width = 1920; + write_metadata.height = 1080; + write_metadata.stride = 1920 * 3; + write_metadata.pixel_format = PixelFormat::RGB24; + write_metadata.capture_timestamp = Clock::now(); + write_metadata.process_timestamp = Clock::now(); + write_metadata.frame_number = 1; + write_metadata.sequence_id = 1; + write_metadata.data_size = 100; + + std::vector write_data(100); + for (size_t i = 0; i < write_data.size(); ++i) { + write_data[i] = static_cast(i & 0xFF); + } + + // 写入帧 + auto write_result = producer.WriteFrame(write_metadata, write_data); + EXPECT_TRUE(write_result.has_value()); + + // 读取帧 + auto read_result = consumer.ReadFrame(100ms); + ASSERT_TRUE(read_result.has_value()); + ASSERT_TRUE(read_result->has_value()); + + const auto& [read_metadata, read_data] = read_result->value(); + + // 验证元数据 + EXPECT_EQ(read_metadata.width, write_metadata.width); + EXPECT_EQ(read_metadata.height, write_metadata.height); + EXPECT_EQ(read_metadata.stride, write_metadata.stride); + EXPECT_EQ(read_metadata.pixel_format, write_metadata.pixel_format); + EXPECT_EQ(read_metadata.frame_number, write_metadata.frame_number); + EXPECT_EQ(read_metadata.data_size, write_metadata.data_size); + + // 验证数据 + ASSERT_EQ(read_data.size(), write_data.size()); + EXPECT_EQ(std::memcmp(read_data.data(), write_data.data(), write_data.size()), 0); +} + +TEST_F(ShmTransportTest, MultipleFrames) +{ + ShmTransport producer; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = kTestBufferCount; + config.max_frame_size = kTestFrameSize; + + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + ShmTransport consumer; + ASSERT_TRUE(consumer.InitializeConsumer(config).has_value()); + + const int num_frames = 10; + + // 写入多帧 + for (int i = 0; i < num_frames; ++i) { + FrameMetadata metadata; + metadata.width = 1920; + metadata.height = 1080; + metadata.stride = 1920 * 3; + metadata.pixel_format = PixelFormat::RGB24; + metadata.capture_timestamp = Clock::now(); + metadata.process_timestamp = Clock::now(); + metadata.frame_number = i; + metadata.sequence_id = i; + metadata.data_size = 100; + + std::vector data(100); + data[0] = static_cast(i); + + auto result = producer.WriteFrame(metadata, data); + ASSERT_TRUE(result.has_value()) << "Failed to write frame " << i; + } + + // 读取多帧 + for (int i = 0; i < num_frames; ++i) { + auto result = consumer.ReadFrame(100ms); + ASSERT_TRUE(result.has_value()) << "Failed to read frame " << i; + ASSERT_TRUE(result->has_value()) << "No frame " << i << " available"; + + const auto& [metadata, data] = result->value(); + EXPECT_EQ(metadata.frame_number, i); + EXPECT_EQ(static_cast(data[0]), i); + } +} + +TEST_F(ShmTransportTest, TryWriteFrameNonBlocking) +{ + ShmTransport producer; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = 2; // 小缓冲区 + config.max_frame_size = 1024; + + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + FrameMetadata metadata; + metadata.width = 100; + metadata.height = 100; + metadata.stride = 300; + metadata.pixel_format = PixelFormat::RGB24; + metadata.capture_timestamp = Clock::now(); + metadata.process_timestamp = Clock::now(); + metadata.frame_number = 0; + metadata.data_size = 100; + + std::vector data(100); + + // 填满缓冲区 + for (size_t i = 0; i < config.buffer_count; ++i) { + auto result = producer.TryWriteFrame(metadata, data); + ASSERT_TRUE(result.has_value()); + EXPECT_TRUE(*result); + } + + // 再次写入应该失败(缓冲区满) + auto result = producer.TryWriteFrame(metadata, data); + ASSERT_TRUE(result.has_value()); + EXPECT_FALSE(*result); +} + +// ========================================================================== +// WriteFrameWithTimeout 超时路径测试 +// ========================================================================== +TEST_F(ShmTransportTest, WriteFrameWithTimeout_BufferFullTimesOut) +{ + ShmTransport producer; + + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = kTestBufferCount; + config.max_frame_size = kTestFrameSize; + config.enable_zero_copy = false; // 禁用零拷贝以使用传统路径 + + auto init_result = producer.InitializeProducer(config); + ASSERT_TRUE(init_result.has_value()) << "Producer initialization failed"; + ASSERT_TRUE(producer.IsInitialized()); + + // 获取初始统计值 + const auto initial_stats = producer.GetStats(); + + // 填满缓冲区 + for (uint32_t i = 0; i < kTestBufferCount; ++i) + { + FrameMetadata metadata{}; + metadata.frame_number = i; + metadata.width = 100; + metadata.height = 100; + metadata.stride = 300; + metadata.pixel_format = PixelFormat::RGB24; + metadata.capture_timestamp = Clock::now(); + metadata.process_timestamp = Clock::now(); + metadata.data_size = static_cast(kTestFrameSize); + + std::vector frame_data(kTestFrameSize, static_cast(i)); + + auto write_result = producer.TryWriteFrame(metadata, frame_data); + ASSERT_TRUE(write_result.has_value()) << "TryWriteFrame error at frame " << i; + ASSERT_TRUE(write_result.value()) << "Failed to write frame " << i << " while filling buffer"; + } + + // 此时环形缓冲区已满,带超时的写入应该失败并更新统计 + FrameMetadata timeout_metadata{}; + timeout_metadata.frame_number = kTestBufferCount; + timeout_metadata.width = 100; + timeout_metadata.height = 100; + timeout_metadata.stride = 300; + timeout_metadata.pixel_format = PixelFormat::RGB24; + timeout_metadata.capture_timestamp = Clock::now(); + timeout_metadata.process_timestamp = Clock::now(); + timeout_metadata.data_size = static_cast(kTestFrameSize); + + std::vector timeout_frame(kTestFrameSize, static_cast(0xFF)); + + const auto timeout = std::chrono::milliseconds(1); + auto write_result = producer.WriteFrameWithTimeout(timeout_metadata, timeout_frame, timeout); + + ASSERT_TRUE(write_result.has_value()) << "WriteFrameWithTimeout returned error: " << static_cast(write_result.error()); + EXPECT_FALSE(write_result.value()) << "WriteFrameWithTimeout should return false when buffer is full"; + + const auto stats = producer.GetStats(); + EXPECT_EQ(stats.write_timeouts.load() - initial_stats.write_timeouts.load(), 1u); +} + +TEST_F(ShmTransportTest, TryReadFrameNonBlocking) +{ + ShmTransport producer; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = kTestBufferCount; + config.max_frame_size = kTestFrameSize; + + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + ShmTransport consumer; + ASSERT_TRUE(consumer.InitializeConsumer(config).has_value()); + + // 尝试读取空缓冲区 + auto result = consumer.TryReadFrame(); + ASSERT_TRUE(result.has_value()); + EXPECT_FALSE(result->has_value()); // 无数据 + + // 写入一帧 + FrameMetadata metadata; + metadata.width = 100; + metadata.height = 100; + metadata.stride = 300; + metadata.pixel_format = PixelFormat::RGB24; + metadata.capture_timestamp = Clock::now(); + metadata.process_timestamp = Clock::now(); + metadata.frame_number = 0; + metadata.data_size = 100; + + std::vector data(100); + + ASSERT_TRUE(producer.WriteFrame(metadata, data).has_value()); + + // 现在应该能读取 + result = consumer.TryReadFrame(); + ASSERT_TRUE(result.has_value()); + ASSERT_TRUE(result->has_value()); +} + +TEST_F(ShmTransportTest, ReadFrameWithCallback) +{ + ShmTransport producer; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = kTestBufferCount; + config.max_frame_size = kTestFrameSize; + + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + ShmTransport consumer; + ASSERT_TRUE(consumer.InitializeConsumer(config).has_value()); + + // 准备测试数据 + FrameMetadata write_metadata; + write_metadata.width = 1920; + write_metadata.height = 1080; + write_metadata.stride = 1920 * 3; + write_metadata.pixel_format = PixelFormat::RGB24; + write_metadata.capture_timestamp = Clock::now(); + write_metadata.process_timestamp = Clock::now(); + write_metadata.frame_number = 42; + write_metadata.data_size = 100; + + std::vector write_data(100); + for (size_t i = 0; i < write_data.size(); ++i) { + write_data[i] = static_cast(i & 0xFF); + } + + ASSERT_TRUE(producer.WriteFrame(write_metadata, write_data).has_value()); + + // 使用回调读取 + bool callback_called = false; + FrameMetadata received_metadata; + std::vector received_data; + + auto callback = [&](const FrameMetadata& metadata, std::span data) { + callback_called = true; + received_metadata = metadata; + received_data.assign(data.begin(), data.end()); + }; + + auto result = consumer.ReadFrameWithCallback(100ms, callback); + EXPECT_TRUE(result.has_value()); + EXPECT_TRUE(callback_called); + + // 验证数据 + EXPECT_EQ(received_metadata.frame_number, write_metadata.frame_number); + EXPECT_EQ(received_data.size(), write_data.size()); + EXPECT_EQ(std::memcmp(received_data.data(), write_data.data(), write_data.size()), 0); +} + +// ========================================================================== +// 统计信息测试 +// ========================================================================== + +TEST_F(ShmTransportTest, TransportStats) +{ + ShmTransport producer; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = kTestBufferCount; + config.max_frame_size = kTestFrameSize; + + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + ShmTransport consumer; + ASSERT_TRUE(consumer.InitializeConsumer(config).has_value()); + + // 初始统计应为零 + auto stats = consumer.GetStats(); + EXPECT_EQ(stats.frames_written, 0); + EXPECT_EQ(stats.frames_read, 0); + + // 写入几帧 + FrameMetadata metadata; + metadata.width = 100; + metadata.height = 100; + metadata.stride = 300; + metadata.pixel_format = PixelFormat::RGB24; + metadata.capture_timestamp = Clock::now(); + metadata.process_timestamp = Clock::now(); + metadata.data_size = 100; + + std::vector data(100); + + for (int i = 0; i < 5; ++i) { + metadata.frame_number = i; + ASSERT_TRUE(producer.WriteFrame(metadata, data).has_value()); + } + + // 读取几帧 + for (int i = 0; i < 3; ++i) { + auto result = consumer.ReadFrame(100ms); + ASSERT_TRUE(result.has_value()); + ASSERT_TRUE(result->has_value()); + } + + // 检查消费者统计 + stats = consumer.GetStats(); + EXPECT_EQ(stats.frames_read, 3); + EXPECT_EQ(stats.bytes_read, 300); + + // 检查缓冲区状态(允许一定误差,因为并发可能导致顺序变化) + auto [used, total] = consumer.GetBufferStatus(); + EXPECT_EQ(total, kTestBufferCount); + EXPECT_GE(used, 1); // 至少还有1帧未读 + EXPECT_LE(used, 2); // 最多2帧未读 +} + +TEST_F(ShmTransportTest, StatsDropRate) +{ + ShmTransportStats stats; + stats.frames_written = 100; + stats.frames_dropped = 10; + + // 丢帧率 = dropped / (written + dropped) = 10 / 110 + EXPECT_DOUBLE_EQ(stats.get_drop_rate(), 10.0 / 110.0); + + stats.frames_dropped = 0; + EXPECT_DOUBLE_EQ(stats.get_drop_rate(), 0.0); + + // 测试 10% 丢帧率的情况:10 dropped / 100 written = 10/110 + stats.frames_written = 90; + stats.frames_dropped = 10; + EXPECT_DOUBLE_EQ(stats.get_drop_rate(), 0.1); // 10/100 = 10% +} + +TEST_F(ShmTransportTest, StatsThroughput) +{ + ShmTransportStats stats; + stats.session_start = Clock::now() - std::chrono::seconds(1); + stats.bytes_written = 1000000; // 1MB + + // 吞吐量应该约为 1MB/s + double throughput = stats.get_throughput_bytes_per_sec(); + EXPECT_GT(throughput, 900000.0); // 允许 10% 误差 + EXPECT_LT(throughput, 1100000.0); +} + +// ========================================================================== +// 边界条件测试 +// ========================================================================== + +TEST_F(ShmTransportTest, EmptyDataWrite) +{ + ShmTransport producer; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = kTestBufferCount; + config.max_frame_size = kTestFrameSize; + + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + FrameMetadata metadata; + metadata.width = 100; + metadata.height = 100; + metadata.stride = 300; + metadata.pixel_format = PixelFormat::RGB24; + metadata.capture_timestamp = Clock::now(); + metadata.process_timestamp = Clock::now(); + metadata.data_size = 0; // 空数据 + + std::vector empty_data; + + // 空数据应该被拒绝(无效帧) + auto result = producer.WriteFrame(metadata, empty_data); + EXPECT_FALSE(result.has_value()); +} + +TEST_F(ShmTransportTest, OversizedFrame) +{ + ShmTransport producer; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = kTestBufferCount; + config.max_frame_size = 1024; // 1KB 限制 + + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + FrameMetadata metadata; + metadata.width = 100; + metadata.height = 100; + metadata.stride = 300; + metadata.pixel_format = PixelFormat::RGB24; + metadata.capture_timestamp = Clock::now(); + metadata.process_timestamp = Clock::now(); + metadata.data_size = 2048; // 超过限制 + + std::vector large_data(2048); + + // 超大帧应该被拒绝 + auto result = producer.WriteFrame(metadata, large_data); + EXPECT_FALSE(result.has_value()); +} + +TEST_F(ShmTransportTest, TimeoutTest) +{ + ShmTransport producer; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = 2; + config.max_frame_size = kTestFrameSize; + + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + ShmTransport consumer; + ASSERT_TRUE(consumer.InitializeConsumer(config).has_value()); + + // 尝试读取不存在的帧(应该超时) + auto start = std::chrono::steady_clock::now(); + auto result = consumer.ReadFrame(50ms); + auto elapsed = std::chrono::steady_clock::now() - start; + + EXPECT_TRUE(result.has_value()); + EXPECT_FALSE(result->has_value()); // 无数据 + EXPECT_GE(elapsed, 50ms); +} + +// ========================================================================== +// 并发测试 +// ========================================================================== + +TEST_F(ShmTransportTest, ConcurrentWriteRead) +{ + ShmTransport producer; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = 16; + config.max_frame_size = kTestFrameSize; + + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + ShmTransport consumer; + ASSERT_TRUE(consumer.InitializeConsumer(config).has_value()); + + constexpr int num_frames = 1000; + std::atomic frames_read{0}; + std::atomic producer_done{false}; + + // 生产者线程 + std::thread producer_thread([&]() { + FrameMetadata metadata; + metadata.width = 100; + metadata.height = 100; + metadata.stride = 300; + metadata.pixel_format = PixelFormat::RGB24; + metadata.capture_timestamp = Clock::now(); + metadata.process_timestamp = Clock::now(); + metadata.data_size = 100; + + std::vector data(100); + + for (int i = 0; i < num_frames; ++i) { + metadata.frame_number = i; + data[0] = static_cast(i & 0xFF); + + auto result = producer.WriteFrameWithTimeout(metadata, data, 1s); + ASSERT_TRUE(result.has_value()); + EXPECT_TRUE(*result); + } + producer_done = true; + }); + + // 消费者线程 + std::thread consumer_thread([&]() { + while (frames_read < num_frames) { + auto result = consumer.ReadFrame(100ms); + if (result && result->has_value()) { + frames_read.fetch_add(1); + } + if (producer_done && !result->has_value()) { + break; + } + } + }); + + producer_thread.join(); + consumer_thread.join(); + + EXPECT_EQ(frames_read, num_frames); +} + +// ========================================================================== +// 控制块测试 +// ========================================================================== + +TEST_F(ShmTransportTest, ControlBlockValidation) +{ + // ShmControlBlock 默认构造时 magic = MAGIC, version = VERSION + // 所以默认就是有效的 + ShmControlBlock block; + EXPECT_TRUE(block.is_valid()); // 默认构造有效 + + // 修改 magic 后应该无效 + block.magic = 0; + EXPECT_FALSE(block.is_valid()); + + // 恢复 magic 但修改 version + block.magic = ShmControlBlock::MAGIC; + block.version = 999; // 错误版本 + EXPECT_FALSE(block.is_valid()); +} + +TEST_F(ShmTransportTest, ControlBlockBufferStatus) +{ + ShmControlBlock block; + block.buffer_count = 4; + block.write_sequence.store(2, std::memory_order_relaxed); + block.read_sequence.store(0, std::memory_order_relaxed); + + EXPECT_EQ(block.used_buffers(), 2); + EXPECT_EQ(block.available_buffers(), 2); + EXPECT_TRUE(block.is_writable()); + EXPECT_TRUE(block.has_readable_data()); + + block.read_sequence.store(2, std::memory_order_relaxed); + EXPECT_EQ(block.used_buffers(), 0); + EXPECT_FALSE(block.has_readable_data()); +} + +// ========================================================================== +// 环形缓冲区溢出测试 +// ========================================================================== +TEST_F(ShmTransportTest, DroppedFramesOnRingOverflow) +{ + // 选择一个很小的 ring 大小,方便在测试中触发溢出 + const size_t kRingCapacityFrames = 4; + const size_t kFramesToWrite = 10; // 明显大于 capacity,确保发生溢出 + + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = static_cast(kRingCapacityFrames); + config.max_frame_size = kTestFrameSize; + config.enable_zero_copy = false; // 使用传统路径以测试超时丢帧 + + // 初始化生产者(不启动消费者,这样不会有消费,ring 会被写满后溢出) + ShmTransport producer; + auto init_result = producer.InitializeProducer(config); + ASSERT_TRUE(init_result.has_value()) << "Producer initialization failed"; + + // 获取初始统计值 + const auto initial_stats = producer.GetStats(); + + FrameMetadata metadata{}; + metadata.width = 100; + metadata.height = 100; + metadata.stride = 100; + metadata.pixel_format = PixelFormat::RGB24; + metadata.data_size = static_cast(kTestFrameSize); + + std::vector frame_data(kTestFrameSize, static_cast(0xAB)); + + // 首先填满缓冲区 + size_t successful_writes = 0; + for (size_t i = 0; i < kRingCapacityFrames; ++i) + { + metadata.frame_number = static_cast(i); + auto result = producer.TryWriteFrame(metadata, frame_data); + ASSERT_TRUE(result.has_value()) << "WriteFrame failed at frame " << i; + if (result.value()) { + successful_writes++; + } + } + EXPECT_EQ(successful_writes, kRingCapacityFrames); + + // 使用带超时的写入,触发丢帧计数 + size_t timeout_count = 0; + for (size_t i = kRingCapacityFrames; i < kFramesToWrite; ++i) + { + metadata.frame_number = static_cast(i); + auto result = producer.WriteFrameWithTimeout(metadata, frame_data, std::chrono::milliseconds(1)); + ASSERT_TRUE(result.has_value()) << "WriteFrameWithTimeout error at frame " << i; + if (!result.value()) { + timeout_count++; + } + } + + // 验证:超时次数应该等于尝试写入的额外帧数 + EXPECT_EQ(timeout_count, kFramesToWrite - kRingCapacityFrames); + + // 获取最终统计值并验证丢帧统计 + const auto final_stats = producer.GetStats(); + EXPECT_EQ(final_stats.write_timeouts.load() - initial_stats.write_timeouts.load(), timeout_count); +} + +// ========================================================================== +// 辅助函数测试 +// ========================================================================== + +TEST_F(ShmTransportTest, MakeShmName) +{ + // Windows: 添加 Local\ 前缀 + // POSIX: 添加 / 前缀 + auto name1 = MakeShmName("test"); + EXPECT_FALSE(name1.empty()); + + auto name2 = MakeShmName(""); + EXPECT_FALSE(name2.empty()); + + // 重复调用应返回相同结果 + auto name3 = MakeShmName("test"); + EXPECT_EQ(name1, name3); +} + +TEST_F(ShmTransportTest, ShmExistsAndRemove) +{ + // 不存在的共享内存 + EXPECT_FALSE(ShmExists("nonexistent_shm_12345")); + + // 创建共享内存 + ShmTransport producer; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = kTestBufferCount; + config.max_frame_size = kTestFrameSize; + + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + // 现在应该存在 + EXPECT_TRUE(ShmExists(test_shm_name_)); + + // 关闭生产者 + ASSERT_TRUE(producer.Shutdown().has_value()); + + // 删除共享内存 + EXPECT_TRUE(ShmRemove(test_shm_name_)); +} + +// ========================================================================== +// 帧头部测试 +// ========================================================================== + +TEST_F(ShmTransportTest, FrameHeaderValidation) +{ + ShmFrameHeader header; + EXPECT_FALSE(header.is_valid()); // 默认构造无效 + + header.magic = ShmFrameHeader::MAGIC; + header.version = 1; + header.data_size = 100; + EXPECT_TRUE(header.is_valid()); + + header.magic = 0; + EXPECT_FALSE(header.is_valid()); +} + +TEST_F(ShmTransportTest, FrameHeaderChecksum) +{ + ShmFrameHeader header; + header.data_size = 10; + + std::array data{ + std::byte{0}, std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}, + std::byte{5}, std::byte{6}, std::byte{7}, std::byte{8}, std::byte{9} + }; + + header.checksum = header.calculate_checksum(data.data()); + EXPECT_TRUE(header.verify_data(data.data())); + + // 修改数据 + data[0] = std::byte{99}; + EXPECT_FALSE(header.verify_data(data.data())); +} + +// ========================================================================== +// 校验和端到端集成测试 +// ========================================================================== + +// 集成测试:启用校验和时应能检测到数据损坏 +TEST_F(ShmTransportTest, ChecksumEnabledDetectsCorruption) +{ + // 安排:创建启用校验和的传输 + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = kTestBufferCount; + config.max_frame_size = kTestFrameSize; + config.enable_checksum = true; // 启用校验和 + + ShmTransport producer; + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + ShmTransport consumer; + ASSERT_TRUE(consumer.InitializeConsumer(config).has_value()); + + const std::string payload = "0123456789ABCDEFGHIJ"; // 20 bytes + FrameMetadata metadata{}; + metadata.width = 100; + metadata.height = 100; + metadata.stride = 100; + metadata.pixel_format = PixelFormat::RGB24; + metadata.frame_number = 1; + metadata.data_size = static_cast(payload.size()); + + // 写入一帧 + auto write_result = producer.WriteFrame(metadata, std::as_bytes(std::span(payload))); + ASSERT_TRUE(write_result.has_value()) << "WriteFrame failed"; + + // 在读取前损坏底层共享内存数据 + // 使用零拷贝API获取帧数据指针并修改 + auto read_buffer = consumer.TryAcquireReadBuffer(); + ASSERT_TRUE(read_buffer.has_value()); + ASSERT_TRUE(read_buffer->has_value()); + + // 获取数据指针并损坏第一个字节 + auto& buffer = read_buffer->value(); + ASSERT_GE(buffer.data.size(), 1u); + const_cast(buffer.data.data())[0] ^= std::byte{0xFF}; // 损坏数据 + + // 释放缓冲区(不调用ReleaseReadBuffer,因为我们损坏了数据) + // 重新初始化消费者以读取损坏的数据 + ShmTransport consumer2; + ASSERT_TRUE(consumer2.InitializeConsumer(config).has_value()); + + // 尝试读取损坏的帧 + auto read_result = consumer2.ReadFrame(100ms); + + // 断言:传输必须检测到损坏并返回ShmCorrupted错误 + EXPECT_FALSE(read_result.has_value()); + EXPECT_EQ(read_result.error(), ShmTransportError::ShmCorrupted); + + // 验证校验和错误计数增加 + const auto stats = consumer2.GetStats(); + EXPECT_EQ(stats.checksum_errors.load(), 1u); +} + +// 集成测试:禁用校验和时不应检测到数据损坏 +TEST_F(ShmTransportTest, ChecksumDisabledDoesNotDetectCorruption) +{ + // 安排:创建禁用校验和的传输 + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = kTestBufferCount; + config.max_frame_size = kTestFrameSize; + config.enable_checksum = false; // 禁用校验和 + + ShmTransport producer; + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + ShmTransport consumer; + ASSERT_TRUE(consumer.InitializeConsumer(config).has_value()); + + const std::string payload = "0123456789ABCDEFGHIJ"; // 20 bytes + FrameMetadata metadata{}; + metadata.width = 100; + metadata.height = 100; + metadata.stride = 100; + metadata.pixel_format = PixelFormat::RGB24; + metadata.frame_number = 1; + metadata.data_size = static_cast(payload.size()); + + // 写入一帧 + auto write_result = producer.WriteFrame(metadata, std::as_bytes(std::span(payload))); + ASSERT_TRUE(write_result.has_value()) << "WriteFrame failed"; + + // 使用零拷贝 API 获取数据指针并损坏第一个字节 + auto read_buffer = consumer.TryAcquireReadBuffer(); + ASSERT_TRUE(read_buffer.has_value()); + ASSERT_TRUE(read_buffer->has_value()); + + auto& buffer = read_buffer->value(); + ASSERT_GE(buffer.data.size(), 1u); + const_cast(buffer.data.data())[0] ^= std::byte{0xFF}; // 损坏数据 + + // 在禁用校验和的情况下,直接读取损坏的数据不应报错 + // 由于我们已经通过零拷贝获取了缓冲区,可以直接验证数据 + // 注意:损坏的数据应该与原始数据不同 + EXPECT_NE(buffer.data[0], std::as_bytes(std::span(payload))[0]); + + // 释放缓冲区 + [[maybe_unused]] auto _ = consumer.ReleaseReadBuffer(buffer.buffer_index); + + // 验证校验和错误计数为0(禁用校验和时不应检测错误) + const auto stats = consumer.GetStats(); + EXPECT_EQ(stats.checksum_errors.load(), 0u); +} + +// ========================================================================== +// 性能基准测试(记录吞吐量,仅做正确性校验,不对性能做硬性要求) +// ========================================================================== + +TEST_F(ShmTransportTest, ThroughputBenchmark) +{ + ShmTransport producer; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = 16; + config.max_frame_size = 1920 * 1080 * 4; // 4K RGBA + + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + ShmTransport consumer; + ASSERT_TRUE(consumer.InitializeConsumer(config).has_value()); + + constexpr int num_frames = 100; + constexpr std::size_t frame_size = 1920 * 1080 * 3; // 1080p RGB24 + + FrameMetadata metadata; + metadata.width = 1920; + metadata.height = 1080; + metadata.stride = 1920 * 3; + metadata.pixel_format = PixelFormat::RGB24; + metadata.capture_timestamp = Clock::now(); + metadata.process_timestamp = Clock::now(); + metadata.data_size = static_cast(frame_size); + + std::vector data(frame_size); + + // 预热 + for (int i = 0; i < 10; ++i) { + [[maybe_unused]] auto _ = producer.WriteFrame(metadata, data); + [[maybe_unused]] auto _2 = consumer.ReadFrame(100ms); + } + + // 基准测试 - 交替写入和读取以避免缓冲区溢出 + auto start = std::chrono::high_resolution_clock::now(); + + for (int i = 0; i < num_frames; ++i) { + metadata.frame_number = i; + auto write_result = producer.WriteFrame(metadata, data); + ASSERT_TRUE(write_result.has_value()); + + // 每写入一帧立即读取,保持缓冲区不溢出 + auto read_result = consumer.ReadFrame(100ms); + ASSERT_TRUE(read_result.has_value()); + ASSERT_TRUE(read_result->has_value()); + } + + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + + // 计算吞吐量 + double total_bytes = static_cast(num_frames * frame_size); + double seconds = duration.count() / 1'000'000.0; + double throughput_mbps = (total_bytes / seconds) / (1024.0 * 1024.0); + + // 仅校验吞吐量为正,避免在不同机器/构建配置下因绝对阈值导致用例不稳定 + GTEST_LOG_(INFO) << "ShmTransport throughput: " << throughput_mbps << " MiB/s"; + EXPECT_GT(throughput_mbps, 0.0); +} + +// ========================================================================== +// 零拷贝延迟基准测试 - 使用真正的零拷贝API +// ========================================================================== +TEST_F(ShmTransportTest, ZeroCopyLatencyBenchmark) +{ + ShmTransport producer; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = 8; + config.max_frame_size = 1024 * 1024; // 1MB 帧 + config.enable_checksum = false; // 禁用CRC校验以提高性能 + + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + ShmTransport consumer; + ASSERT_TRUE(consumer.InitializeConsumer(config).has_value()); + + constexpr int num_frames = 1000; + constexpr std::size_t frame_size = 1024 * 1024; // 1MB + + FrameMetadata metadata; + metadata.width = 1024; + metadata.height = 1024; + metadata.stride = 1024; + metadata.pixel_format = PixelFormat::RGB24; + metadata.data_size = static_cast(frame_size); + + // 测试1: 零拷贝写入吞吐量(生产者端测量) + // 使用消费者线程边读边写,避免缓冲区溢出 + { + std::atomic write_count{0}; + std::atomic writer_done{false}; + + // 启动消费者线程清空缓冲区 + std::thread consumer_thread([&]() { + while (!writer_done.load(std::memory_order_acquire) || write_count.load(std::memory_order_acquire) > 0) { + auto read_result = consumer.TryAcquireReadBuffer(); + if (read_result && read_result->has_value()) { + [[maybe_unused]] auto _ = consumer.ReleaseReadBuffer(read_result->value().buffer_index); + write_count.fetch_sub(1, std::memory_order_relaxed); + } else { + std::this_thread::yield(); + } + } + }); + + auto start = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < num_frames; ++i) { + // 使用带超时的API等待缓冲区可用 + auto buffer_result = producer.AcquireWriteBuffer(100ms); + ASSERT_TRUE(buffer_result.has_value()); + ASSERT_TRUE(buffer_result->has_value()); + + auto& buffer = buffer_result->value(); + // 直接写入共享内存(零拷贝) + std::memset(buffer.data, static_cast(i & 0xFF), frame_size); + + metadata.frame_number = i; + auto commit_result = producer.CommitWriteBuffer(buffer.buffer_index, metadata, frame_size); + ASSERT_TRUE(commit_result.has_value()); + + write_count.fetch_add(1, std::memory_order_relaxed); + } + auto end = std::chrono::high_resolution_clock::now(); + auto duration_ns = std::chrono::duration_cast(end - start).count(); + + writer_done.store(true, std::memory_order_release); + consumer_thread.join(); + + double total_mb = static_cast(num_frames * frame_size) / (1024.0 * 1024.0); + double seconds = duration_ns / 1'000'000'000.0; + double write_mbps = total_mb / seconds; + double write_latency_ns = static_cast(duration_ns) / num_frames; + + std::cout << "[ZeroCopy] Write Throughput: " << write_mbps << " MB/s" << std::endl; + std::cout << "[ZeroCopy] Write Latency: " << write_latency_ns << " ns/frame" << std::endl; + } + + // 测试2: 零拷贝读取吞吐量(消费者端测量) + { + // 先填充缓冲区(使用生产者线程边写边读避免阻塞) + std::atomic written{0}; + std::atomic fill_done{false}; + + std::thread fill_consumer([&]() { + while (!fill_done.load(std::memory_order_acquire) || written.load(std::memory_order_acquire) > 0) { + auto read_result = consumer.TryAcquireReadBuffer(); + if (read_result && read_result->has_value()) { + [[maybe_unused]] auto _ = consumer.ReleaseReadBuffer(read_result->value().buffer_index); + written.fetch_sub(1, std::memory_order_relaxed); + } else { + std::this_thread::yield(); + } + } + }); + + for (int i = 0; i < num_frames; ++i) { + auto buffer_result = producer.AcquireWriteBuffer(100ms); + ASSERT_TRUE(buffer_result.has_value()); + ASSERT_TRUE(buffer_result->has_value()); + + auto& buffer = buffer_result->value(); + std::memset(buffer.data, static_cast(i & 0xFF), frame_size); + metadata.frame_number = i; + [[maybe_unused]] auto _ = producer.CommitWriteBuffer(buffer.buffer_index, metadata, frame_size); + written.fetch_add(1, std::memory_order_relaxed); + } + + fill_done.store(true, std::memory_order_release); + fill_consumer.join(); + + // 重新填充用于读取测试 + for (int i = 0; i < std::min(num_frames, 8); ++i) { + auto buffer_result = producer.AcquireWriteBuffer(100ms); + if (buffer_result && buffer_result->has_value()) { + auto& buffer = buffer_result->value(); + std::memset(buffer.data, static_cast(i & 0xFF), frame_size); + metadata.frame_number = i; + [[maybe_unused]] auto _ = producer.CommitWriteBuffer(buffer.buffer_index, metadata, frame_size); + } + } + + auto start = std::chrono::high_resolution_clock::now(); + int read_count = 0; + while (read_count < num_frames) { + auto result = consumer.TryAcquireReadBuffer(); + if (result && result->has_value()) { + // 直接读取共享内存(零拷贝)- 验证数据 + auto& buffer = result->value(); + EXPECT_EQ(buffer.data.size(), frame_size); + + // 释放缓冲区 + [[maybe_unused]] auto _ = consumer.ReleaseReadBuffer(buffer.buffer_index); + read_count++; + + // 补充一帧数据保持缓冲区满 + auto wb = producer.TryAcquireWriteBuffer(); + if (wb && wb->has_value()) { + std::memset(wb->value().data, static_cast(read_count & 0xFF), frame_size); + metadata.frame_number = read_count; + [[maybe_unused]] auto _c = producer.CommitWriteBuffer(wb->value().buffer_index, metadata, frame_size); + } + } + } + auto end = std::chrono::high_resolution_clock::now(); + auto duration_ns = std::chrono::duration_cast(end - start).count(); + + double total_mb = static_cast(num_frames * frame_size) / (1024.0 * 1024.0); + double seconds = duration_ns / 1'000'000'000.0; + double read_mbps = total_mb / seconds; + double read_latency_ns = static_cast(duration_ns) / num_frames; + + std::cout << "[ZeroCopy] Read Throughput: " << read_mbps << " MB/s" << std::endl; + std::cout << "[ZeroCopy] Read Latency: " << read_latency_ns << " ns/frame" << std::endl; + + // 仅记录性能指标,不做硬性断言以避免CI环境抖动导致失败 + GTEST_LOG_(INFO) << "ZeroCopy read throughput: " << read_mbps << " MB/s"; + GTEST_LOG_(INFO) << "ZeroCopy read latency: " << read_latency_ns << " ns/frame"; + } + + // 测试3: 并发零拷贝读写吞吐量(双工模式) + { + std::atomic write_count{0}; + std::atomic read_count{0}; + std::atomic start_flag{false}; + std::atomic writer_done{false}; + + std::thread writer([&]() { + while (!start_flag.load(std::memory_order_acquire)) { + std::this_thread::yield(); + } + for (int i = 0; i < num_frames; ++i) { + auto buffer_result = producer.AcquireWriteBuffer(100ms); + if (buffer_result && buffer_result->has_value()) { + auto& buffer = buffer_result->value(); + std::memset(buffer.data, static_cast(i & 0xFF), frame_size); + metadata.frame_number = i; + if (producer.CommitWriteBuffer(buffer.buffer_index, metadata, frame_size).has_value()) { + write_count.fetch_add(1, std::memory_order_relaxed); + } + } + } + writer_done.store(true, std::memory_order_release); + }); + + std::thread reader([&]() { + while (!start_flag.load(std::memory_order_acquire)) { + std::this_thread::yield(); + } + int local_read = 0; + while (local_read < num_frames) { + auto result = consumer.TryAcquireReadBuffer(); + if (result && result->has_value()) { + if (consumer.ReleaseReadBuffer(result->value().buffer_index).has_value()) { + read_count.fetch_add(1, std::memory_order_relaxed); + local_read++; + } + } else if (writer_done.load(std::memory_order_acquire) && + read_count.load(std::memory_order_acquire) >= write_count.load(std::memory_order_acquire)) { + // 写入完成且已读完所有数据 + break; + } else { + std::this_thread::yield(); + } + } + }); + + auto start = std::chrono::high_resolution_clock::now(); + start_flag.store(true, std::memory_order_release); + + writer.join(); + reader.join(); + + auto end = std::chrono::high_resolution_clock::now(); + auto duration_ns = std::chrono::duration_cast(end - start).count(); + + double total_mb = static_cast((write_count + read_count) * frame_size) / (1024.0 * 1024.0); + double seconds = duration_ns / 1'000'000'000.0; + double duplex_mbps = total_mb / seconds; + + std::cout << "[ZeroCopy] Duplex Throughput: " << duplex_mbps << " MB/s" << std::endl; + std::cout << "[ZeroCopy] Frames written: " << write_count << ", read: " << read_count << std::endl; + + EXPECT_EQ(write_count, num_frames); + EXPECT_EQ(read_count, num_frames); + } +} + +// ========================================================================== +// 微秒级单帧延迟测试(使用零拷贝API) +// ========================================================================== +TEST_F(ShmTransportTest, MicrosecondLatencyTest) +{ + // NOTE: + // This test is extremely sensitive to system load, CPU scaling, and + // virtualization. To avoid CI flakiness, it is gated by an environment + // variable and will be skipped unless explicitly enabled. + const char* perf_env = std::getenv("SHM_TRANSPORT_PERF_TEST"); + if (!perf_env || std::strcmp(perf_env, "1") != 0) + { + GTEST_SKIP() << "Skipping MicrosecondLatencyTest; " + << "enable with SHM_TRANSPORT_PERF_TEST=1 in a dedicated " + << "performance environment."; + } + + ShmTransport producer; + ShmTransportConfig config; + config.shm_name = test_shm_name_; + config.buffer_count = 4; + config.max_frame_size = 1024 * 1024; // 1MB 小帧 + config.enable_checksum = false; // 禁用CRC校验以提高性能 + + ASSERT_TRUE(producer.InitializeProducer(config).has_value()); + + ShmTransport consumer; + ASSERT_TRUE(consumer.InitializeConsumer(config).has_value()); + + constexpr int num_samples = 100; + constexpr std::size_t frame_size = 1024 * 1024; // 1MB + + FrameMetadata metadata; + metadata.width = 1024; + metadata.height = 1024; + metadata.stride = 1024; + metadata.pixel_format = PixelFormat::RGB24; + metadata.data_size = static_cast(frame_size); + + // 预热 + for (int i = 0; i < 10; ++i) { + auto wb = producer.TryAcquireWriteBuffer(); + if (wb && wb->has_value()) { + [[maybe_unused]] auto _ = producer.CommitWriteBuffer(wb->value().buffer_index, metadata, 1024); + } + auto rb = consumer.TryAcquireReadBuffer(); + if (rb && rb->has_value()) { + [[maybe_unused]] auto _ = consumer.ReleaseReadBuffer(rb->value().buffer_index); + } + } + + // 测量单帧传输延迟(写+读往返)- 使用零拷贝API + std::vector latencies_us; + latencies_us.reserve(num_samples); + + for (int i = 0; i < num_samples; ++i) { + auto start = std::chrono::high_resolution_clock::now(); + + // 零拷贝写入 + metadata.frame_number = i; + auto write_buffer = producer.TryAcquireWriteBuffer(); + ASSERT_TRUE(write_buffer.has_value()); + ASSERT_TRUE(write_buffer->has_value()); + + // 直接写入共享内存 + std::memset(write_buffer->value().data, static_cast(i & 0xFF), 1024); + + auto write_result = producer.CommitWriteBuffer(write_buffer->value().buffer_index, metadata, 1024); + ASSERT_TRUE(write_result.has_value()); + + // 零拷贝读取 + auto read_buffer = consumer.TryAcquireReadBuffer(); + ASSERT_TRUE(read_buffer.has_value()); + ASSERT_TRUE(read_buffer->has_value()); + + auto read_result = consumer.ReleaseReadBuffer(read_buffer->value().buffer_index); + ASSERT_TRUE(read_result.has_value()); + + auto end = std::chrono::high_resolution_clock::now(); + + double latency_us = std::chrono::duration_cast(end - start).count() / 1000.0; + latencies_us.push_back(latency_us); + } + + // 计算统计信息 + std::sort(latencies_us.begin(), latencies_us.end()); + double min_latency = latencies_us.front(); + double max_latency = latencies_us.back(); + double median_latency = latencies_us[num_samples / 2]; + double avg_latency = std::accumulate(latencies_us.begin(), latencies_us.end(), 0.0) / num_samples; + + // 计算P99 + int p99_index = static_cast(num_samples * 0.99); + double p99_latency = latencies_us[p99_index]; + + std::cout << "[MicroLatency] Min: " << min_latency << " us" << std::endl; + std::cout << "[MicroLatency] Max: " << max_latency << " us" << std::endl; + std::cout << "[MicroLatency] Median: " << median_latency << " us" << std::endl; + std::cout << "[MicroLatency] Average: " << avg_latency << " us" << std::endl; + std::cout << "[MicroLatency] P99: " << p99_latency << " us" << std::endl; + + // 仅在性能测试环境中验证严格延迟目标 + // 使用零拷贝API,P99延迟应该 < 10μs(对于1KB数据) + EXPECT_LT(p99_latency, 10.0); + EXPECT_LT(median_latency, 5.0); +} diff --git a/develop_plan/ROADMAP.md b/develop_plan/ROADMAP.md index 557c08b..cf04371 100644 --- a/develop_plan/ROADMAP.md +++ b/develop_plan/ROADMAP.md @@ -92,7 +92,7 @@ ### v0.2.0-alpha.3:传输层 -- [ ] `src/l0_sensing/shm_transport.cpp`:共享内存传输 +- [x] `src/l0_sensing/shm_transport.cpp`:共享内存传输 ### v0.2.0-alpha.4:基础设施