☰
SysDVR 源码构建完全指南:sysmodule、设置工具与多平台 C 客户端的编译实战
2026/10/13 11:00:34 网站建设 项目流程
  • 嵌入式
  • 音视频
  • 游戏开发

【免费下载链接】SysDVR

Stream switch games to your PC via USB or network

项目地址:https://gitcode.com/gh_mirrors/sy/SysDVR
点击查看免费下载

本文以仓库中的 building.md 为主体,系统讲解 SysDVR 三大组件(Switch 端 sysmodule、设置 homebrew、跨平台 C# 客户端)从源码编译的完整流程,并结合 sysmodule/Makefile、Client/Client.csproj、Client/Platform/DynamicLibraryLoader.cs 等源码,深入解析USB_ONLY编译开关、NativeAOT 发布参数与原生库加载机制,帮助你在 Switch 和 PC/Android 上完整复现官方构建流程。

一、项目结构与构建总览

SysDVR 由三个必须分别独立构建的组件组成:

组件位置目标平台构建工具链
Sysmodule(系统模块)sysmodule/SwitchdevkitA64 (DevKitPro)
Settings homebrew(设置应用)SysDVRConfig/SwitchdevkitA64 (DevKitPro)
Client(客户端)Client/Windows / Linux / macOS / Androiddotnet CLI(NativeAOT)

官方说明文档基于 v6.0 编写,当前仓库版本号为 6.3(见 SysDVRConfig/Makefile 中APP_VERSION := 6.3与 Client/release_version.txt)。文档同时提醒:对于更早或更新的版本,可以查阅 CI 构建脚本确认构建步骤的变化,CI 脚本始终与代码保持同步。

构建的核心差异在于:Switch 侧是标准的 Switch homebrew 项目,用Makefile驱动;客户端是跨平台 C# 项目,由于支持平台范围广,构建过程明显更复杂。下文分两部分展开。

二、Switch 侧构建:sysmodule 与 Settings

这两个组件是常规 Switch homebrew 项目,使用 DevKitPro 的devkitA64工具链编译。

2.1 基本构建方式

两个组件各自目录下的Makefile基于 libnx 模板,要求环境中设置DEVKITPRO环境变量,否则会直接报错:

ifeq ($(strip $(DEVKITPRO)),) $(error "Please set DEVKITPRO in your environment. export DEVKITPRO=<path to>/devkitpro") endif TOPDIR ?= $(CURDIR) include $(DEVKITPRO)/libnx/switch_rules

见 sysmodule/Makefile 与 SysDVRConfig/Makefile。设置好DEVKITPRO后,在各组件目录下执行make(可加-j并行)即可。两个 Makefile 的关键差异:

  • sysmodule/Makefile:TARGET := $(notdir $(CURDIR)),依赖 sysmodule/sysmodule.json 这份 NPDM 配置,最终产物为.nsp(ExeFS PFS0 系统模块),标题 ID 为0x00FF0000A53BB665;
  • SysDVRConfig/Makefile:TARGET := SysDVR-conf,且启用了ROMFS := romfs,会把 SysDVRConfig/romfs/ 下的字体(fonts/)与多语言字符串(strings/,如英文、简中、繁中等 8 种 JSON 文案)打进 NRO 的 RomFS,最终产物为.nro设置应用。

2.2USB_ONLY:最重要的功能编译开关

building.md 明确指出:Switch 项目使用Makefile构建,存在若干特殊编译标志,其中最相关的是USB_ONLY,通过以下方式开启:

make -j DEFINES="-DUSB_ONLY"

这会产出仅支持 USB 传输的 SysDVR 版本。除此之外还有一些用于调试的标志(如UDP_LOGGING、FILE_LOGGING),官方声明它们不稳定且不在构建文档中详述。

从源码可以确认USB_ONLY的具体影响面。在 sysmodule/source/sysmodule/main.c#L61-L73 中有明确注释:

/* Build with USB_ONLY to have a smaller impact on memory, it will only stream via USB and won't support the config app. Note that memory savings don't come from the INNER_HEAP_SIZE variable but from the statically allocated stacks and buffers needed for network modes */

即 USB-only 版本的内存节省并非来自堆大小,而是省掉了网络模式所需的那些静态分配的线程栈与缓冲区。同时 sysmodule/source/core.h#L12-L13 定义了依赖推导:

#define NEEDS_FS (!defined(USB_ONLY) || FILE_LOGGING) #define NEEDS_SOCKETS (!defined(USB_ONLY) || UDP_LOGGING)

即未定义USB_ONLY时链接文件系统与 socket 支持;而USB_ONLY版本下RTSP.c、TCPmode.c、ipc.c等网络相关文件在预处理阶段即被整体排除(各文件首行形如#if !defined(USB_ONLY))。

另一个可验证的构建事实是版本标识串。sysmodule/source/core.c#L28-L33 会根据构建方式写入不同的VERSION_TAG:

// We use the version tag to detect the build in the settings app. #ifdef USB_ONLY const char VERSION_TAG[] = "SYSDVR_BUILD_USB_ONLY/" SYSDVR_VERSION_STRING "/" SYSDVR_PROTOCOL_VERSION; #else const char VERSION_TAG[] = "SYSDVR_BUILD_FULL/" SYSDVR_VERSION_STRING "/" SYSDVR_PROTOCOL_VERSION; #endif

设置应用(SysDVR-conf)正是靠这个字符串判断当前运行的是完整版还是 USB-only 版。

2.3 官方发布脚本:两种构建如何组装

仓库根目录的 ReleaseSysmodule.sh 完整演示了官方如何构建并打包这两个组件,值得作为参照:

cd SysDVRConfig make clean make -j 8 cd ../sysmodule make clean make -j 8 # 校验完整版版本串没有被链接器剥离 if [ -z "$(strings sysmodule.nsp | grep 'SYSDVR_BUILD_FULL')" ]; then echo "Error: SysDVR version string not found in sysmodule.elf" exit 1 fi # 组装 Atmosphere 安装目录(Main 目录) cp sysmodule.nsp ../SysmoduleRelease/Main/atmosphere/contents/00FF0000A53BB665/exefs.nsp echo . > ../SysmoduleRelease/Main/atmosphere/contents/00FF0000A53BB665/flags/boot2.flag cp ../SysDVRConfig/SysDVR-conf.nro ../SysmoduleRelease/Main/switch/SysDVR-conf.nro # 再构建 USB-only 版并组装 USB 目录 make clean make -j 8 DEFINES="-DUSB_ONLY"

几个值得注意的细节:

  1. 版本串校验:自 6.2.1 起,发布包会嵌入带构建类型标识的字符串(SYSDVR_BUILD_FULL/SYSDVR_BUILD_USB_ONLY),脚本用strings校验它确实存在于产物中,防止链接器误删;
  2. boot2.flag:两种产物都写入空的flags/boot2.flag,声明这是 boot2 阶段加载的系统模块;
  3. 两种安装布局:Main 目录包含完整版 sysmodule + 设置 NRO +config/sysdvr/rtsp默认模式标记;USB 目录只包含 USB-only 版,并使用 sysmodule/toolbox.usb.json。

三、Client 构建:NativeAOT 与平台构建脚本

3.1 NativeAOT 构建模型

客户端是一个跨平台 C# 项目,支持 Windows、Linux、macOS(Intel 与 ARM)以及 Android。它被构建为NativeAOT 应用:每个平台产出自己的原生可执行文件,无需安装 dotnet 运行时即可启动。官方以 NativeAOT 二进制形式发布的平台包括:

  • Windows x64
  • Linux x64(flatpak 分发)
  • macOS x64 与 arm64
  • Android arm64

要支持其他平台(最典型的是 ARM 版 Linux,例如 Raspberry Pi),则需要自行从源码构建客户端。各平台的构建脚本统一放在 Client/Platform/ 目录下。

从 Client/Client.csproj 可以看到 AOT 是默认行为:

<TargetFramework>net9.0</TargetFramework> ... <PublishAot>true</PublishAot>

需要说明的一点版本适配:building.md 写于 v6.0,当时要求 dotnet 8.0;而当前代码库已升级到net9.0目标框架(配合RollForward>Major),flatpak 清单 Client/Platform/Linux/com.github.exelix11.sysdvr.json 中也使用了org.freedesktop.Sdk.Extension.dotnet9SDK 扩展。因此在当前仓库上构建,请使用 .NET 9 SDK。

3.2 托管构建与开发调试

文档说明:客户端用 .NET csproj 构建,仅用 dotnet CLI 构建即可得到一个托管版本(依赖 dotnet 运行时),这非常适合日常开发——只要装好了原生库,就能在 Visual Studio 或 VSCode 里直接运行和调试客户端。而各平台构建脚本除了发布外,还负责下载原生依赖:

平台构建脚本
WindowsClient/Platform/BuildWindows.bat
macOSClient/Platform/BuildMacos.sh
Linux(flatpak)Client/Platform/Linux/build-flatpak.sh
AndroidClient/Platform/Android/buildbinaries.sh

3.3 Windows 构建脚本解析

BuildWindows.bat 的流程是:

  1. 下载 ffmpeg 5.1.2 Windows x64 LGPL 共享构建,把*.dll拷入Resources\win-x64\native\;
  2. 下载 CimguiSDL2Crossr2的 Windows x64 包(cimgui_r2标记文件用于幂等判断);
  3. 下载 SDL2_image 2.6.3 与 libusb 1.0.26 的预编译 DLL;
  4. 执行发布与打包:
dotnet publish -c Release -r win-x64 /p:SysDvrTarget=windows --self-contained 7z a Client.7z .\bin\Release\net9.0\win-x64\publish\*

注意这里显式传了--self-contained。这与 csproj 中的一段特殊配置相呼应(Client/Client.csproj#L40-L46):

<PropertyGroup Condition="$(SysDvrTarget)=='windows'"> <!-- Due to antimalware vendors flagging AOT releases --> <!-- Windows releases now don't use AOT anymore but framework-independent dotnet releases --> <PublishAot>false</PublishAot> <PublishTrimmed>true</PublishTrimmed> </PropertyGroup>

从源码注释看,由于部分杀毒软件会误报 AOT 发布的可执行文件,Windows 发布版改为框架无关(framework-dependent)+ 裁剪的发布方式,代价是需要随包分发整个 .NET 框架。

3.4 macOS 构建脚本解析

BuildMacos.sh 的特点是依赖全部采用 "fat" 二进制(同时包含 x64 与 arm64 代码),一次下载两种架构共用:

  1. 下载 CimguiSDL2Cross r2 的 macOS fat 包、SDL2_image 2.6.3 的 dmg(用dmg2img+7z解出SDL2_image.framework内的 dylib)、ffmpeg-kit 5.1 的 xcframework(解出libavcodec/libavformat/libswscale等 7 个 dylib)以及 libusb;
  2. 处理签名问题:先codesign --remove-signature去除全部签名,再对 arm64 Mac 做 ad-hoc 重签名(codesign -s -),否则 Apple Silicon 上库根本无法加载;
  3. 分别发布两个架构并各打一个 zip:
dotnet publish -c Release -r osx-x64 /p:SysDvrTarget=macos dotnet publish -c Release -r osx-arm64 /p:SysDvrTarget=macos

3.5 Linux flatpak 构建:依赖从源码编译

build-flatpak.sh 只有核心两行,真正的工作由 com.github.exelix11.sysdvr.json 这份 manifest 完成:

git clone --depth 1 https://github.com/flathub/shared-modules.git flatpak-builder --user --install --install-deps-from=flathub tmp com.github.exelix11.sysdvr.json --force-clean flatpak build-bundle ~/.local/share/flatpak/repo SysDVR-Client.flatpak com.github.exelix11.sysdvr --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo

manifest 的关键设计:

  • 运行时基于org.freedesktop.Platform24.08,挂载org.freedesktop.Sdk.Extension.dotnet9提供 dotnet 9 SDK;
  • ffmpeg 5.1.7 在沙箱内从源码编译(--enable-shared --disable-static --disable-programs),libusb 使用 flathub 的shared-modules预构建模块;
  • SDL2 / SDL2_image 等由运行时提供,构建命令里通过软链接把它们接成客户端约定的库名(ln -s .../libSDL2-2.0.so.0 /app/lib/libSDL2.so),并把 CimguiSDL2Cross 的cimgui.so拷入runtimes/$RUNTIME/native;
  • finish-args中--device=all是关键:客户端要用到 USB 设备访问(libusb 直通),配合仓库中的 udev 规则 Client/Platform/Linux/sysdvr.rules。

3.6SysDvrTarget:平台条件编译参数

文档特别提醒:Android 构建是特殊的,因为它需要额外的/p:SysDvrTarget=android参数来包含平台特定代码;该参数对其他平台同样可用但当前未使用,应以现有构建脚本为准。

在 Client/Client.csproj 中可以看到SysDvrTarget的完整消费方式:

<PropertyGroup Condition="$(SysDvrTarget)!='android'"> <!--On android we build as a shared library that is loaded by the SDL java shell--> <OutputType>Exe</OutputType> </PropertyGroup> <PropertyGroup Condition="$(SysDvrTarget)=='android'"> <DefineConstants>$(DefineConstants);ANDROID_LIB</DefineConstants> <PublishAotUsingRuntimePack>true</PublishAotUsingRuntimePack> </PropertyGroup> <ItemGroup Condition="$(SysDvrTarget)=='android'"> <!--Android needs a proper soname property or it will refuse to load the library--> <LinkerArg Include="-Wl,-soname,SysDVR-Client.so,--undefined-version" /> </ItemGroup>

即 Android 下客户端被编译为共享库SysDVR-Client.so(由 SDL 的 Java shell 加载),其余平台为独立可执行文件;ANDROID_LIB条件编译宏会同时影响 C# 侧代码(例如--legacy命令行选项就标注了#if !ANDROID_LIB,因为桌面播放器选项在 Android 上没有意义,见 Client/CommandLineOptions.cs#L59-L62)。

四、原生依赖:下载、放置与加载机制

4.1 依赖清单

无论托管还是 AOT 构建,运行客户端前都需要备齐以下原生依赖的可执行文件:

  • ffmpeg(libavcodec、libavformat、libavutil、libswscale 等)
  • SDL2
  • SDL_image
  • LibUSB(仅当你打算使用 USB 传输时)
  • CimguiSDL2Cross:开源 cimgui 项目的 fork,带有若干 SDL 特定改动,由该仓库自身的 CI 构建并以二进制包形式发布,供各构建脚本下载。

官方之所以在所有发布包中捆绑这些库,文档给的解释很直白:要求非 Windows 用户自行正确安装这些库"基本上是不可能的"。

4.2 库的放置位置:runtimes/<平台>/native

构建脚本会把下载的原生库放到Platform/runtimes/<your_platform>/native目录(例如 64 位 ARM Linux 对应runtimes/linux-arm64/native),msbuild 编译过程会自动把它们包含进最终的可执行文件输出目录。

这一点在 Client/Client.csproj#L83-L88 中有对应的工程机制:Platform/Resources/**下的内容被声明为ContentWithTargetPath,按runtimes\...目标路径复制到输出目录。

而实际运行时的查找顺序,可以在 Client/Platform/DynamicLibraryLoader.cs 的FindNativeLibrary中逐条印证:

// Is an override set ? if (!string.IsNullOrWhiteSpace(LibLoaderOverride)) yield return Path.Combine(LibLoaderOverride, libraryName); // Is this a library we provide ? yield return Path.Combine(BundledOsNativeFolder, libraryName); // Flatpak uses this path if (Program.IsLinux) yield return Path.Combine("/app/lib", libraryName); // Maybe it's in the working directory yield return libraryName;

即依次尝试:手动覆盖目录 → 捆绑的runtimes/<os><arch>/native(BundledOsNativeFolder由win/osx/linux加-x64/-arm64等后缀拼成)→ flatpak 的/app/lib→ 当前工作目录。全部落空后TryLoadLibrary还会回退到操作系统默认的库搜索(NativeLibrary.TryLoad(libraryName),并尝试lib前缀与去除版本号的重试)。这正是 building.md 所说"native 文件夹只是首选加载位置,找不到时回退到系统路径"的实现依据——所以如果你在自己电脑上运行,直接通过包管理器安装依赖库、不复制二进制也能工作。

唯一的例外是CimguiSDL2Cross:它完全自定义,必须自行构建,按其自身仓库的说明用 cmake 编译即可。即使没有它,客户端带--legacy选项运行仍然可用,只是会失去 GUI(进入旧版播放器,实现见 Client/App/LegacyPlayer.cs)。

4.3 用--debug dynlib排查加载问题

文档给出的排障手段是带--debug dynlib参数启动客户端。对照 Client/CommandLineOptions.cs#L66-L76 的调试选项定义:

new ("dynlib", "Enable verbose logging for native library loading", x => x.DynLib = true),

当DynLib开启时,DynamicLibraryLoader.cs 会把每个候选路径的存在性检查、Trying to load ...、加载失败等信息打印到控制台(例如Candidate library does not exist ...、Attempt to load ... without a path...),定位"缺哪个库、在哪个路径找的"一目了然。顺带一提,其他调试选项(log、fps、stats、keyframe、nal、nosync等,见 Client/Core/DebugOptions.cs)也走同一套--debug参数体系。

五、Android 构建:AOT 共享库 + NDK 源码依赖 + Gradle 打包

Client/Platform/Android/readme.md 把整个流程概括为三步:① 把 SysDVR-Client 构建成原生 AOT 库;② 收集全部原生库的源码;③ 用 gradle / Android Studio 构建完整应用。buildbinaries.sh只负责前两步,最终 APK 仍需 gradle 或 Android Studio 构建。

5.1 buildbinaries.sh 做了什么

Client/Platform/Android/buildbinaries.sh 的核心工作是把各依赖以源码形式落进app/jni(Android NDK 的推荐做法):

  1. SDL 2.28.3:下载源码包,把src/include/Android.mk移入app/jni/SDL,并应用补丁 patches/SDL_android.c.patch——该补丁给 SDL 增加了AndroidGetJavaVM()、Android_JNI_GetEnv_NoAttach()等导出,供客户端 C 层直接取 JNI 环境;
  2. SDL_image 2.6.3:源码放入app/jni/SDL_Image;
  3. CimguiSDL2Cross r2:git clone --depth=1 -b r2,执行其apply_patches.sh后把 cimgui 源码与 Android.mk 移入app/jni/cimgui;
  4. libusb 1.0.26:源码放入app/jni/libusb,并用 patches/libusb_Android.mk 覆盖其Android.mk;
  5. ffmpeg-kit 5.1 LTS:下载 AAR 后手工裁剪——删除 x86/x86_64 以及非 arm64 的libffmpegkit.so,把 APK 体积从约 120MB 压到 60MB,产物存为app/libs/ffmpeg-kit-full-5.1.LTS_trimmed.aar。

脚本还检查了资源符号链接:app/src/main/assets必须链接到../Resources/resources,否则直接退出。

5.2 发布两个 ABI 的 AOT 库

脚本最后回到客户端根目录执行两次 AOT 发布,把产物作为预编译共享库放进 jni 树:

dotnet publish -r linux-bionic-arm64 /p:SysDvrTarget=android mv bin/Release/net9.0/linux-bionic-arm64/publish/SysDVR-Client.so \ Platform/Android/app/jni/SysDVR-Client/arm64-v8a/ dotnet publish -r linux-bionic-arm /p:SysDvrTarget=android mv bin/Release/net9.0/linux-bionic-arm/publish/SysDVR-Client.so \ Platform/Android/app/jni/SysDVR-Client/armeabi-v7a/

注意脚本中对环境的提示:PATH 中Android NDK 的 clang 必须排在系统 clang 之前(如export PATH=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/:$PATH),且需要ANDROID_NDK_HOME。

NDK 侧的接线在两个 Makefile 中完成。app/jni/SysDVR-Client/Android.mk 把预编译的SysDVR-Client.so声明为 PREBUILT_SHARED_LIBRARY,并声明其对SDL2 SDL2_image cimgui log的依赖;app/jni/src/Android.mk 则构建 Java shell 的 C 层入口(main.c usb.c thread.c native.c),链接全部六个组件:

LOCAL_SHARED_LIBRARIES := SDL2 SDL2_image cimgui SysDVR-Client-prebuilt libusb1.0

5.3 Java 壳与运行时依赖校验

Android 应用的 Java 层是一个继承自 SDL 的 Activity(sysdvrActivity.java 继承org.libsdl.app.SDLActivity),对 Android 而言 SysDVR 只是一个不透明的 native 库。C# 侧在ANDROID_LIB宏下还有专门的启动自检:Client/Platform/DynamicLibraryLoader.cs#L40-L67 会通过 JNI 查询 APK 的实际 native 库路径(SysGetDynamicLibInfo),并维护一份"已知坏构建哈希"黑名单——因为 CI 的 Android 原生库必须与构建 APK 的工具链一致,缺少符号的构建会在加载时失败;检查不通过会抛出 "Native android dependencies are missing..." 异常并提示 Android 上只支持 arm64 构建。

六、构建排障要点汇总

结合 building.md 与源码,常见构建/运行问题的核对清单:

  1. make直接报错:检查DEVKITPRO是否指向 devkitpro 根目录、libnx 是否安装;
  2. USB-only 版与完整版搞混:用strings sysmodule.nsp | grep SYSDVR_BUILD看版本串,或检查设置应用(USB-only 版不支持设置应用,见 main.c 注释);
  3. 客户端 AOT 发布产物缺原生库:确认依赖位于runtimes/<平台>/native,或直接运行--debug dynlib观察加载日志;
  4. macOS Apple Silicon 加载失败:确认 dylib 经过 ad-hoc 重签名,且没有用 Rosetta 运行 Intel 版(客户端启动时会主动检测并报错,见 DynamicLibraryLoader.cs#L181-L190);
  5. flatpak 版 USB 无权限:确认 manifest 的--device=all与 udev 规则 sysdvr.rules 已安装;
  6. Androiddotnet publish失败:确认 NDK clang 在 PATH 中优先、ANDROID_NDK_HOME已设置,且app/src/main/assets符号链接完好。

七、小结

SysDVR 的构建体系可以概括为"两边工具链、一套 AOT 参数":Switch 侧是两个标准的 devkitA64 Makefile 工程,make -j DEFINES="-DUSB_ONLY"切换功能形态,产物按 ReleaseSysmodule.sh 的流程组装成 Atmosphere 安装目录;客户端侧是 net9.0 的 NativeAOT 发布,通过/p:SysDvrTarget=区分桌面可执行文件与 Android 共享库,原生依赖统一收敛到runtimes/<平台>/native并由 DynamicLibraryLoader.cs 按"捆绑目录 → flatpak/app/lib→ 工作目录 → 系统路径"的顺序解析加载。掌握以上流程后,你就可以在当前仓库中为任意目标平台(包括官方未发布的 ARM Linux 设备)复现完整构建,并用--debug dynlib、strings校验等手段自行排查问题。

  • 嵌入式
  • 音视频
  • 游戏开发

【免费下载链接】SysDVR

Stream switch games to your PC via USB or network

项目地址:https://gitcode.com/gh_mirrors/sy/SysDVR
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询